Added unittests for nested inlinefuncs, and deprecation warning for the old version of {inlinefunc syntax.

This commit is contained in:
Griatch 2015-11-17 22:39:26 +01:00
parent 641846e855
commit 0a4c217b3e
3 changed files with 90 additions and 19 deletions

View file

@ -45,7 +45,7 @@ should be returned. The inlinefunc should never cause a traceback.
import re import re
from django.conf import settings from django.conf import settings
from evennia.utils import utils from evennia.utils import utils, logger
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
@ -61,7 +61,7 @@ def pad(text, *args, **kwargs):
fillchar = ' ' fillchar = ' '
for iarg, arg in enumerate(args): for iarg, arg in enumerate(args):
if iarg == 0: if iarg == 0:
width = int(arg) if arg.isdigit() else width width = int(arg) if arg.strip().isdigit() else width
elif iarg == 1: elif iarg == 1:
align = arg if arg in ('c', 'l', 'r') else align align = arg if arg in ('c', 'l', 'r') else align
elif iarg == 2: elif iarg == 2:
@ -80,7 +80,7 @@ def crop(text, *args, **kwargs):
suffix = "[...]" suffix = "[...]"
for iarg, arg in enumerate(args): for iarg, arg in enumerate(args):
if iarg == 0: if iarg == 0:
width = int(arg) if arg.isdigit() else width width = int(arg) if arg.strip().isdigit() else width
elif iarg == 1: elif iarg == 1:
suffix = arg suffix = arg
else: else:
@ -97,7 +97,7 @@ def wrap(text, *args, **kwargs):
indent = 0 indent = 0
for iarg, arg in enumerate(args): for iarg, arg in enumerate(args):
if iarg == 0: if iarg == 0:
width = int(arg) if arg.isdigit() else width width = int(arg) if arg.strip().isdigit() else width
elif iarg == 1: elif iarg == 1:
indent = int(arg) if arg.isdigit() else indent indent = int(arg) if arg.isdigit() else indent
return utils.wrap(text, width=width, indent=indent) return utils.wrap(text, width=width, indent=indent)
@ -242,6 +242,7 @@ def parse_inlinefunc(text, strip=False, session=None):
for part in _FUNCSPLIT_REGEX.split("".join(stack)): for part in _FUNCSPLIT_REGEX.split("".join(stack)):
starttag = _FUNCSTART_REGEX.match(part) starttag = _FUNCSTART_REGEX.match(part)
if starttag: if starttag:
logger.log_dep("The {func()-style inlinefunc is deprecated. Use the $func{} form instead.")
startname = starttag.group(1) startname = starttag.group(1)
part = _execute_inline_single_function(startname, part, session) part = _execute_inline_single_function(startname, part, session)
outstack.append(part) outstack.append(part)

View file

@ -4,9 +4,9 @@ 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, ...}
``` ```
where any arg can be another $funcname() call. where any arg can be another $funcname{} call.
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.
@ -50,18 +50,56 @@ 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
def pad(*args, **kwargs):
"""
Pad to width. pad(text, width, align, fillchar)
"""
text, width, align, fillchar = "", 78, 'c', ' '
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
if nargs > 3:
fillchar = args[3]
return utils.pad(text, width=width, align=align, fillchar=fillchar)
def crop(text, *args, **kwargs):
"""
Crop to width. crop(text, width=78, suffix='[...]')
"""
text = width, suffix = "", 78, "[...]"
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
suffix = args[2]
return utils.crop(text, width=width, suffix=suffix)
# 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: %s>" % (args[0]), _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):
_INLINE_FUNCS.update(utils.all_from_module(module)) _INLINE_FUNCS.update(utils.all_from_module(module))
# remove
_INLINE_FUNCS.pop("inline_func_parse", None) # remove the core function if we include examples in this module itself
#_INLINE_FUNCS.pop("inline_func_parse", None)
# The stack size is a security measure. Set to <=0 to disable. # The stack size is a security measure. Set to <=0 to disable.
@ -74,7 +112,7 @@ except AttributeError:
_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"""(?<!\\)''(?P<singlequote>.*?)(?<!\\)\''| # unescaped '' (escapes all inside them) _RE_TOKEN = re.compile(r"""(?<!\\)'''(?P<singlequote>.*?)(?<!\\)'''| # unescaped '' (escapes all inside them)
(?<!\\)\"\"\"(?P<triplequote>.*?)(?<!\\)\"\"\"| # unescaped triple quote (escapes all inside them) (?<!\\)\"\"\"(?P<triplequote>.*?)(?<!\\)\"\"\"| # unescaped triple quote (escapes all inside them)
(?P<comma>(?<!\\)\,)| # unescaped , (argument lists) - this is thrown away (?P<comma>(?<!\\)\,)| # unescaped , (argument lists) - this is thrown away
(?P<end>(?<!\\)\})| # unescaped ) (end of function call) (?P<end>(?<!\\)\})| # unescaped ) (end of function call)
@ -138,10 +176,11 @@ def parse_inlinefunc(string, strip=False, **kwargs):
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.
kwargs (any, optional): This will be passed on to all found inlinefuncs as Kwargs:
part of their call signature. This is to be called by Evennia or the session (Session): This is sent to this function by Evennia when triggering
coder and is used to make various game states available, such as it. It is passed to the inlinefunc.
the session of the user triggering the parse, character etc. kwargs (any): All other kwargs are also passed on to the inlinefunc.
""" """
global _PARSING_CACHE global _PARSING_CACHE
@ -159,7 +198,6 @@ def parse_inlinefunc(string, strip=False, **kwargs):
ncallable = 0 ncallable = 0
for match in _RE_TOKEN.finditer(string): for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict() gdict = match.groupdict()
print "gdict:", gdict
if gdict["singlequote"]: if gdict["singlequote"]:
stack.append(gdict["singlequote"]) stack.append(gdict["singlequote"])
elif gdict["triplequote"]: elif gdict["triplequote"]:
@ -204,6 +242,10 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# the rest # the rest
stack.append(gdict["rest"]) stack.append(gdict["rest"])
if ncallable > 0:
# this means not all inlinefuncs were complete
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)
@ -213,6 +255,7 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# run the stack recursively # run the stack recursively
def _run_stack(item): def _run_stack(item):
retval = item
if isinstance(item, tuple): if isinstance(item, tuple):
if strip: if strip:
return "" return ""
@ -227,11 +270,9 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# all other args should merge into one string # all other args should merge into one string
args[-1] += _run_stack(arg) args[-1] += _run_stack(arg)
# execute the inlinefunc at this point or strip it. # execute the inlinefunc at this point or strip it.
return "" if strip else utils.to_str(func(*args, **kwargs)) retval = "" if strip else func(*args, **kwargs)
else: return utils.to_str(retval, force_string=True)
return item
# execute the stack from the cache # execute the stack from the cache
print "_PARSING_CACHE[string]:", _PARSING_CACHE[string]
return "".join(_run_stack(item) for item in _PARSING_CACHE[string]) return "".join(_run_stack(item) for item in _PARSING_CACHE[string])

View file

@ -309,3 +309,32 @@ class TestTextToHTMLparser(TestCase):
self.assertEqual(self.parser.convert_urls('</span>http://example.com/<span class="red">'), self.assertEqual(self.parser.convert_urls('</span>http://example.com/<span class="red">'),
'</span><a href="http://example.com/" target="_blank">http://example.com/</a><span class="red">') '</span><a href="http://example.com/" target="_blank">http://example.com/</a><span class="red">')
from evennia.utils import nested_inlinefuncs
class TestNestedInlineFuncs(TestCase):
"Test the nested inlinefunc module"
def test_nofunc(self):
self.assertEqual(nested_inlinefuncs.parse_inlinefunc(
"as$382ewrw w we w werw,|44943}"),
"as$382ewrw w we w werw,|44943}")
def test_incomplete(self):
self.assertEqual(nested_inlinefuncs.parse_inlinefunc(
"testing $blah{without an ending."),
"testing $blah{without an ending.")
def test_single_func(self):
self.assertEqual(nested_inlinefuncs.parse_inlinefunc(
"this is a test with $pad{centered, 20} text in it."),
"this is a test with centered text in it.")
def test_nested(self):
self.assertEqual(nested_inlinefuncs.parse_inlinefunc(
"this $crop{is a test with $pad{padded, 20} text in $pad{pad2, 10} a crop, 80}"),
"this is a test with padded text in pad2 a crop")
def test_escaped(self):
self.assertEqual(nested_inlinefuncs.parse_inlinefunc(
"this should be $pad{'''escaped,''' and \"\"\"instead,\"\"\" cropped $crop{with a long,5} text., 80}"),
"this should be escaped, and instead, cropped with text. ")