Handle missing characters in inlinefunc as per #1498

This commit is contained in:
Griatch 2018-06-16 22:14:28 +02:00
parent 2eaae6ac48
commit d047f2b919

View file

@ -1,471 +1,471 @@
""" """
Inline functions (nested form). Inline functions (nested form).
This parser accepts nested inlinefunctions on the form This parser accepts nested inlinefunctions on the form
``` ```
$funcname(arg, arg, ...) $funcname(arg, arg, ...)
``` ```
embedded in any text where any arg can be another $funcname{} call. embedded in any text where any arg can be another $funcname{} call.
This functionality is turned off by default - to activate, This functionality is turned off by default - to activate,
`settings.INLINEFUNC_ENABLED` must be set to `True`. `settings.INLINEFUNC_ENABLED` must be set to `True`.
Each token starts with "$funcname(" where there must be no space Each token starts with "$funcname(" where there must be no space
between the $funcname and (. It ends with a matched ending parentesis. between the $funcname and (. It ends with a matched ending parentesis.
")". ")".
Inside the inlinefunc definition, one can use `\` to escape. This is Inside the inlinefunc definition, one can use `\` to escape. This is
mainly needed for escaping commas in flowing text (which would mainly needed for escaping commas in flowing text (which would
otherwise be interpreted as an argument separator), or to escape `}` otherwise be interpreted as an argument separator), or to escape `}`
when not intended to close the function block. Enclosing text in when not intended to close the function block. Enclosing text in
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
also escape *everything* within without needing to escape individual also escape *everything* within without needing to escape individual
characters. characters.
The available inlinefuncs are defined as global-level functions in The available inlinefuncs are defined as global-level functions in
modules defined by `settings.INLINEFUNC_MODULES`. They are identified modules defined by `settings.INLINEFUNC_MODULES`. They are identified
by their function name (and ignored if this name starts with `_`). They by their function name (and ignored if this name starts with `_`). They
should be on the following form: should be on the following form:
```python ```python
def funcname (*args, **kwargs): def funcname (*args, **kwargs):
# ... # ...
``` ```
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
`*args` tuple. This will be populated by the arguments given to the `*args` tuple. This will be populated by the arguments given to the
inlinefunc in-game - the only part that will be available from inlinefunc in-game - the only part that will be available from
in-game. `**kwargs` are not supported from in-game but are only used in-game. `**kwargs` are not supported from in-game but are only used
internally by Evennia to make details about the caller available to internally by Evennia to make details about the caller available to
the function. The kwarg passed to all functions is `session`, the the function. The kwarg passed to all functions is `session`, the
Sessionobject for the object seeing the string. This may be `None` if Sessionobject for the object seeing the string. This may be `None` if
the string is sent to a non-puppetable object. The inlinefunc should the string is sent to a non-puppetable object. The inlinefunc should
never raise an exception. never raise an exception.
There are two reserved function names: There are two reserved function names:
- "nomatch": This is called if the user uses a functionname that is - "nomatch": This is called if the user uses a functionname that is
not registered. The nomatch function will get the name of the not registered. The nomatch function will get the name of the
not-found function as its first argument followed by the normal not-found function as its first argument followed by the normal
arguments to the given function. If not defined the default effect is arguments to the given function. If not defined the default effect is
to print `<UNKNOWN>` to replace the unknown function. to print `<UNKNOWN>` to replace the unknown function.
- "stackfull": This is called when the maximum nested function stack is reached. - "stackfull": This is called when the maximum nested function stack is reached.
When this happens, the original parsed string is returned and the result of When this happens, the original parsed string is returned and the result of
the `stackfull` inlinefunc is appended to the end. By default this is an the `stackfull` inlinefunc is appended to the end. By default this is an
error message. error message.
Error handling: Error handling:
Syntax errors, notably not completely closing all inlinefunc Syntax errors, notably not completely closing all inlinefunc
blocks, will lead to the entire string remaining unparsed. blocks, will lead to the entire string remaining unparsed.
""" """
import re import re
from django.conf import settings from django.conf import settings
from evennia.utils import utils from evennia.utils import utils
# example/testing inline functions # example/testing inline functions
def pad(*args, **kwargs): def pad(*args, **kwargs):
""" """
Inlinefunc. Pads text to given width. Inlinefunc. Pads text to given width.
Args: Args:
text (str, optional): Text to pad. text (str, optional): Text to pad.
width (str, optional): Will be converted to integer. Width width (str, optional): Will be converted to integer. Width
of padding. of padding.
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'. align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
fillchar (str, optional): Character used for padding. Defaults to a space. fillchar (str, optional): Character used for padding. Defaults to a space.
Kwargs: Kwargs:
session (Session): Session performing the pad. session (Session): Session performing the pad.
Example: Example:
`$pad(text, width, align, fillchar)` `$pad(text, width, align, fillchar)`
""" """
text, width, align, fillchar = "", 78, 'c', ' ' text, width, align, fillchar = "", 78, 'c', ' '
nargs = len(args) nargs = len(args)
if nargs > 0: if nargs > 0:
text = args[0] text = args[0]
if nargs > 1: if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78 width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2: if nargs > 2:
align = args[2] if args[2] in ('c', 'l', 'r') else 'c' align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
if nargs > 3: if nargs > 3:
fillchar = args[3] fillchar = args[3]
return utils.pad(text, width=width, align=align, fillchar=fillchar) return utils.pad(text, width=width, align=align, fillchar=fillchar)
def crop(*args, **kwargs): def crop(*args, **kwargs):
""" """
Inlinefunc. Crops ingoing text to given widths. Inlinefunc. Crops ingoing text to given widths.
Args: Args:
text (str, optional): Text to crop. text (str, optional): Text to crop.
width (str, optional): Will be converted to an integer. Width of width (str, optional): Will be converted to an integer. Width of
crop in characters. crop in characters.
suffix (str, optional): End string to mark the fact that a part suffix (str, optional): End string to mark the fact that a part
of the string was cropped. Defaults to `[...]`. of the string was cropped. Defaults to `[...]`.
Kwargs: Kwargs:
session (Session): Session performing the crop. session (Session): Session performing the crop.
Example: Example:
`$crop(text, width=78, suffix='[...]')` `$crop(text, width=78, suffix='[...]')`
""" """
text, width, suffix = "", 78, "[...]" text, width, suffix = "", 78, "[...]"
nargs = len(args) nargs = len(args)
if nargs > 0: if nargs > 0:
text = args[0] text = args[0]
if nargs > 1: if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78 width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2: if nargs > 2:
suffix = args[2] suffix = args[2]
return utils.crop(text, width=width, suffix=suffix) return utils.crop(text, width=width, suffix=suffix)
def clr(*args, **kwargs): def clr(*args, **kwargs):
""" """
Inlinefunc. Colorizes nested text. Inlinefunc. Colorizes nested text.
Args: Args:
startclr (str, optional): An ANSI color abbreviation without the startclr (str, optional): An ANSI color abbreviation without the
prefix `|`, such as `r` (red foreground) or `[r` (red background). prefix `|`, such as `r` (red foreground) or `[r` (red background).
text (str, optional): Text text (str, optional): Text
endclr (str, optional): The color to use at the end of the string. Defaults endclr (str, optional): The color to use at the end of the string. Defaults
to `|n` (reset-color). to `|n` (reset-color).
Kwargs: Kwargs:
session (Session): Session object triggering inlinefunc. session (Session): Session object triggering inlinefunc.
Example: Example:
`$clr(startclr, text, endclr)` `$clr(startclr, text, endclr)`
""" """
text = "" text = ""
nargs = len(args) nargs = len(args)
if nargs > 0: if nargs > 0:
color = args[0].strip() color = args[0].strip()
if nargs > 1: if nargs > 1:
text = args[1] text = args[1]
text = "|" + color + text text = "|" + color + text
if nargs > 2: if nargs > 2:
text += "|" + args[2].strip() text += "|" + args[2].strip()
else: else:
text += "|n" text += "|n"
return text return text
def null(*args, **kwargs): def null(*args, **kwargs):
return args[0] if args else '' return args[0] if args else ''
# we specify a default nomatch function to use if no matching func was # we specify a default nomatch function to use if no matching func was
# found. This will be overloaded by any nomatch function defined in # found. This will be overloaded by any nomatch function defined in
# the imported modules. # the imported modules.
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>", _INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} "stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"}
# load custom inline func modules. # load custom inline func modules.
for module in utils.make_iter(settings.INLINEFUNC_MODULES): for module in utils.make_iter(settings.INLINEFUNC_MODULES):
try: try:
_INLINE_FUNCS.update(utils.callables_from_module(module)) _INLINE_FUNCS.update(utils.callables_from_module(module))
except ImportError as err: except ImportError as err:
if module == "server.conf.inlinefuncs": if module == "server.conf.inlinefuncs":
# a temporary warning since the default module changed name # a temporary warning since the default module changed name
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
"be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err) "be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err)
else: else:
raise raise
# The stack size is a security measure. Set to <=0 to disable. # The stack size is a security measure. Set to <=0 to disable.
try: try:
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
except AttributeError: except AttributeError:
_STACK_MAXSIZE = 20 _STACK_MAXSIZE = 20
# regex definitions # regex definitions
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call) _RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call)
_RE_TOKEN = re.compile(r""" _RE_TOKEN = re.compile(r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # unescaped single-triples (escapes all inside them) (?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # unescaped single-triples (escapes all inside them)
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # unescaped normal triple quotes (escapes all inside them) (?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # unescaped normal triple quotes (escapes all inside them)
(?P<comma>(?<!\\)\,)| # unescaped , (argument separator) (?P<comma>(?<!\\)\,)| # unescaped , (argument separator)
(?P<end>(?<!\\)\))| # unescaped ) (end of function call) (?P<end>(?<!\\)\))| # unescaped ) (end of function call)
(?P<start>(?<!\\)\$\w+\()| # unescaped $funcname( (start of function call) (?P<start>(?<!\\)\$\w+\()| # unescaped $funcname( (start of function call)
(?P<escaped>\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text (?P<escaped>\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text
(?P<rest>[\w\s.-\/#@$\>\<!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""", (?P<rest>[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]@\$\\\+\<\>?]+|\"{1}|\'{1}) # everything else """,
re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL)
# Cache for function lookups.
# Cache for function lookups. _PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
class ParseStack(list):
class ParseStack(list): """
""" Custom stack that always concatenates strings together when the
Custom stack that always concatenates strings together when the strings are added next to one another. Tuples are stored
strings are added next to one another. Tuples are stored separately and None is used to mark that a string should be broken
separately and None is used to mark that a string should be broken up into a new chunk. Below is the resulting stack after separately
up into a new chunk. Below is the resulting stack after separately appending 3 strings, None, 2 strings, a tuple and finally 2
appending 3 strings, None, 2 strings, a tuple and finally 2 strings:
strings:
[string + string + string,
[string + string + string, None
None string + string,
string + string, tuple,
tuple, string + string]
string + string]
"""
"""
def __init__(self, *args, **kwargs):
def __init__(self, *args, **kwargs): super(ParseStack, self).__init__(*args, **kwargs)
super(ParseStack, self).__init__(*args, **kwargs) # always start stack with the empty string
# always start stack with the empty string list.append(self, "")
list.append(self, "") # indicates if the top of the stack is a string or not
# indicates if the top of the stack is a string or not self._string_last = True
self._string_last = True
def __eq__(self, other):
def __eq__(self, other): return (super(ParseStack).__eq__(other) and
return (super(ParseStack).__eq__(other) and hasattr(other, "_string_last") and self._string_last == other._string_last)
hasattr(other, "_string_last") and self._string_last == other._string_last)
def __ne__(self, other):
def __ne__(self, other): return not self.__eq__(other)
return not self.__eq__(other)
def append(self, item):
def append(self, item): """
""" The stack will merge strings, add other things as normal
The stack will merge strings, add other things as normal """
""" if isinstance(item, basestring):
if isinstance(item, basestring): if self._string_last:
if self._string_last: self[-1] += item
self[-1] += item else:
else: list.append(self, item)
list.append(self, item) self._string_last = True
self._string_last = True else:
else: # everything else is added as normal
# everything else is added as normal list.append(self, item)
list.append(self, item) self._string_last = False
self._string_last = False
class InlinefuncError(RuntimeError):
class InlinefuncError(RuntimeError): pass
pass
def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs):
def parse_inlinefunc(string, strip=False, available_funcs=None, **kwargs): """
""" Parse the incoming string.
Parse the incoming string.
Args:
Args: string (str): The incoming string to parse.
string (str): The incoming string to parse. strip (bool, optional): Whether to strip function calls rather than
strip (bool, optional): Whether to strip function calls rather than execute them.
execute them. available_funcs (dict, optional): Define an alternative source of functions to parse for.
available_funcs (dict, optional): Define an alternative source of functions to parse for. If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
If unset, use the functions found through `settings.INLINEFUNC_MODULES`. Kwargs:
Kwargs: session (Session): This is sent to this function by Evennia when triggering
session (Session): This is sent to this function by Evennia when triggering it. It is passed to the inlinefunc.
it. It is passed to the inlinefunc. kwargs (any): All other kwargs are also passed on to the inlinefunc.
kwargs (any): All other kwargs are also passed on to the inlinefunc.
"""
""" global _PARSING_CACHE
global _PARSING_CACHE
usecache = False
usecache = False if not available_funcs:
if not available_funcs: available_funcs = _INLINE_FUNCS
available_funcs = _INLINE_FUNCS usecache = True
usecache = True
if usecache and string in _PARSING_CACHE:
if usecache and string in _PARSING_CACHE: # stack is already cached
# stack is already cached stack = _PARSING_CACHE[string]
stack = _PARSING_CACHE[string] elif not _RE_STARTTOKEN.search(string):
elif not _RE_STARTTOKEN.search(string): # if there are no unescaped start tokens at all, return immediately.
# if there are no unescaped start tokens at all, return immediately. return string
return string else:
else: # no cached stack; build a new stack and continue
# no cached stack; build a new stack and continue stack = ParseStack()
stack = ParseStack()
# process string on stack
# process string on stack ncallable = 0
ncallable = 0 for match in _RE_TOKEN.finditer(string):
for match in _RE_TOKEN.finditer(string): gdict = match.groupdict()
gdict = match.groupdict() if gdict["singlequote"]:
if gdict["singlequote"]: stack.append(gdict["singlequote"])
stack.append(gdict["singlequote"]) elif gdict["doublequote"]:
elif gdict["doublequote"]: stack.append(gdict["doublequote"])
stack.append(gdict["doublequote"]) elif gdict["end"]:
elif gdict["end"]: if ncallable <= 0:
if ncallable <= 0: stack.append(")")
stack.append(")") continue
continue args = []
args = [] while stack:
while stack: operation = stack.pop()
operation = stack.pop() if callable(operation):
if callable(operation): if not strip:
if not strip: stack.append((operation, [arg for arg in reversed(args)]))
stack.append((operation, [arg for arg in reversed(args)])) ncallable -= 1
ncallable -= 1 break
break else:
else: args.append(operation)
args.append(operation) elif gdict["start"]:
elif gdict["start"]: funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) try:
try: # try to fetch the matching inlinefunc from storage
# try to fetch the matching inlinefunc from storage stack.append(available_funcs[funcname])
stack.append(available_funcs[funcname]) except KeyError:
except KeyError: stack.append(available_funcs["nomatch"])
stack.append(available_funcs["nomatch"]) stack.append(funcname)
stack.append(funcname) ncallable += 1
ncallable += 1 elif gdict["escaped"]:
elif gdict["escaped"]: # escaped tokens
# escaped tokens token = gdict["escaped"].lstrip("\\")
token = gdict["escaped"].lstrip("\\") stack.append(token)
stack.append(token) elif gdict["comma"]:
elif gdict["comma"]: if ncallable > 0:
if ncallable > 0: # commas outside strings and inside a callable are
# commas outside strings and inside a callable are # used to mark argument separation - we use None
# used to mark argument separation - we use None # in the stack to indicate such a separation.
# in the stack to indicate such a separation. stack.append(None)
stack.append(None) else:
else: # no callable active - just a string
# no callable active - just a string stack.append(",")
stack.append(",") else:
else: # the rest
# the rest stack.append(gdict["rest"])
stack.append(gdict["rest"])
if ncallable > 0:
if ncallable > 0: # this means not all inlinefuncs were complete
# this means not all inlinefuncs were complete return string
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): # if stack is larger than limit, throw away parsing
# if stack is larger than limit, throw away parsing return string + gdict["stackfull"](*args, **kwargs)
return string + gdict["stackfull"](*args, **kwargs) elif usecache:
elif usecache: # cache the stack - we do this also if we don't check the cache above
# cache the stack - we do this also if we don't check the cache above _PARSING_CACHE[string] = stack
_PARSING_CACHE[string] = stack
# run the stack recursively
# run the stack recursively def _run_stack(item, depth=0):
def _run_stack(item, depth=0): retval = item
retval = item if isinstance(item, tuple):
if isinstance(item, tuple): if strip:
if strip: return ""
return "" else:
else: func, arglist = item
func, arglist = item args = [""]
args = [""] for arg in arglist:
for arg in arglist: if arg is None:
if arg is None: # an argument-separating comma - start a new arg
# an argument-separating comma - start a new arg args.append("")
args.append("") else:
else: # all other args should merge into one string
# all other args should merge into one string args[-1] += _run_stack(arg, depth=depth + 1)
args[-1] += _run_stack(arg, depth=depth + 1) # execute the inlinefunc at this point or strip it.
# execute the inlinefunc at this point or strip it. kwargs["inlinefunc_stack_depth"] = depth
kwargs["inlinefunc_stack_depth"] = depth retval = "" if strip else func(*args, **kwargs)
retval = "" if strip else func(*args, **kwargs) return utils.to_str(retval, force_string=True)
return utils.to_str(retval, force_string=True)
print("STACK:\n{}".format(stack))
# execute the stack # execute the stack
return "".join(_run_stack(item) for item in stack) return "".join(_run_stack(item) for item in stack)
# #
# Nick templating # Nick templating
# #
""" """
This supports the use of replacement templates in nicks: This supports the use of replacement templates in nicks:
This happens in two steps: This happens in two steps:
1) The user supplies a template that is converted to a regex according 1) The user supplies a template that is converted to a regex according
to the unix-like templating language. to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement 2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline). strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers, 3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given. these are replaced with the arguments actually given.
@desc $1 $2 $3 @desc $1 $2 $3
This will be converted to the following regex: This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+) \@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch) Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*? * matches anything (non-greedy) -> .*?
? matches any single character -> ? matches any single character ->
[seq] matches any entry in sequence [seq] matches any entry in sequence
[!seq] matches entries not in sequence [!seq] matches entries not in sequence
Custom arg markers Custom arg markers
$N argument position (1-99) $N argument position (1-99)
""" """
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") import fnmatch
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") _RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ") _RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
""" def initialize_nick_templates(in_template, out_template):
Initialize the nick templates for matching and remapping a string. """
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition. Args:
out_template (str): The template to be used to replace the string in_template (str): The template to be used for nick recognition.
matched by the in_template. out_template (str): The template to be used to replace the string
matched by the in_template.
Returns:
regex (regex): Regex to match against strings Returns:
template (str): Template with markers {arg1}, {arg2}, etc for regex (regex): Regex to match against strings
replacement using the standard .format method. template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
NickTemplateInvalid: If the in/out template does not have a matching Raises:
number of $args. NickTemplateInvalid: If the in/out template does not have a matching
number of $args.
"""
# create the regex for in_template """
regex_string = fnmatch.translate(in_template) # create the regex for in_template
n_inargs = len(_RE_NICK_ARG.findall(regex_string)) regex_string = fnmatch.translate(in_template)
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string) n_inargs = len(_RE_NICK_ARG.findall(regex_string))
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string) regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
# create the out_template
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template) # create the out_template
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template)) # validate the tempaltes - they should at least have the same number of args
if n_inargs != n_outargs: n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
print n_inargs, n_outargs if n_inargs != n_outargs:
raise NickTemplateInvalid raise NickTemplateInvalid
return re.compile(regex_string), template_string return re.compile(regex_string), template_string
def parse_nick_template(string, template_regex, outtemplate): def parse_nick_template(string, template_regex, outtemplate):
""" """
Parse a text using a template and map it to another template Parse a text using a template and map it to another template
Args: Args:
string (str): The input string to processj string (str): The input string to processj
template_regex (regex): A template regex created with template_regex (regex): A template regex created with
initialize_nick_template. initialize_nick_template.
outtemplate (str): The template to which to map the matches outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2, produced by the template_regex. This should have $1, $2,
etc to match the regex. etc to match the regex.
""" """
match = template_regex.match(string) match = template_regex.match(string)
if match: if match:
return outtemplate.format(**match.groupdict()) return outtemplate.format(**match.groupdict())
return string return string