Replace inlinefunc parser with FuncParser mostly

This commit is contained in:
Griatch 2021-03-17 23:44:08 +01:00
parent b832280f37
commit 8924304d80
10 changed files with 294 additions and 322 deletions

View file

@ -7,7 +7,7 @@ from django.db.models import Q, Min, Max
from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search, logger
from evennia.utils import create, utils, search, logger, funcparser
from evennia.utils.utils import (
inherits_from,
class_from_module,
@ -22,10 +22,11 @@ from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw as ansi_raw
from evennia.utils.inlinefuncs import raw as inlinefunc_raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
_FUNCPARSER = funcparser.FuncParser(settings.INLINEFUNC_MODULES)
# limit symbol import for API
__all__ = (
"ObjManipCommand",
@ -2385,7 +2386,7 @@ class CmdExamine(ObjManipCommand):
value = utils.to_str(value)
if crop:
value = utils.crop(value)
value = inlinefunc_raw(ansi_raw(value))
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
if category:
return f"{attr}[{category}] = {value}"
else:

View file

@ -151,6 +151,7 @@ def add(*args, **kwargs):
val1, val2 = args[0], args[1]
# try to convert to python structures, otherwise, keep as strings
try:
print("val1", val1, type(literal_eval(val1)))
val1 = literal_eval(val1.strip())
except Exception:
pass

View file

@ -30,6 +30,7 @@ from evennia.utils.utils import (
)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger
from evennia.utils.funcparser import FuncParser
from evennia.utils import inlinefuncs, dbserialize
from evennia.utils.evtable import EvTable
@ -758,9 +759,11 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
available_functions = PROT_FUNCS if available_functions is None else available_functions
result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions, stacktrace=stacktrace, testing=testing, **kwargs
)
result = FuncParser(available_functions).parse(value, raise_errors=True, **kwargs)
# result = inlinefuncs.parse_inlinefunc(
# value, available_funcs=available_functions, stacktrace=stacktrace, testing=testing, **kwargs
# )
err = None
try:

View file

@ -377,7 +377,7 @@ class TestProtFuncs(EvenniaTest):
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
self.assertEqual(
protlib.protfunc_parser("$add('''[1,2,3]''', '''[4,5,6]''')"), [1, 2, 3, 4, 5, 6]
protlib.protfunc_parser("$add('[1,2,3]', '[4,5,6]')"), [1, 2, 3, 4, 5, 6]
)
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar")

View file

@ -28,7 +28,7 @@ from evennia.utils.utils import (
from evennia.server.portal import amp
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
from evennia.utils.inlinefuncs import parse_inlinefunc
# from evennia.utils.inlinefuncs import parse_inlinefunc
from codecs import decode as codecs_decode
_INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
@ -59,6 +59,9 @@ _DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
_MODEL_MAP = None
_FUNCPARSER = None
# input handlers
@ -171,6 +174,11 @@ class SessionHandler(dict):
applied.
"""
global _FUNCPARSER
if not _FUNCPARSER:
from evennia.utils.funcparser import FuncParser
_FUNCPARSER = FuncParser(settings.INLINEFUNC_MODULES, raise_errors=True)
options = kwargs.pop("options", None) or {}
raw = options.get("raw", False)
strip_inlinefunc = options.get("strip_inlinefunc", False)
@ -204,7 +212,8 @@ class SessionHandler(dict):
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
# only parse inlinefuncs on the outgoing path (sessionhandler->)
data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)
return str(data)
elif (

View file

@ -149,9 +149,13 @@ class FuncParser:
"""
for funcname, clble in callables.items():
mapping = inspect.getfullargspec(clble)
assert mapping.varargs, f"Parse-func callable '{funcname}' does not support *args."
assert mapping.varkw, f"Parse-func callable '{funcname}' does not support **kwargs."
try:
mapping = inspect.getfullargspec(clble)
except TypeError:
logger.log_trace(f"Could not run getfullargspec on {funcname}: {clble}")
else:
assert mapping.varargs, f"Parse-func callable '{funcname}' does not support *args."
assert mapping.varkw, f"Parse-func callable '{funcname}' does not support **kwargs."
def execute(self, parsedfunc, raise_errors=False, **reserved_kwargs):
"""
@ -207,7 +211,7 @@ class FuncParser:
raise
return str(parsedfunc)
def parse(self, string, raise_errors=False, **reserved_kwargs):
def parse(self, string, raise_errors=False, escape=False, strip=False, **reserved_kwargs):
"""
Use parser to parse a string that may or may not have `$funcname(*args, **kwargs)`
- style tokens in it. Only the callables used to initiate the parser
@ -215,10 +219,14 @@ class FuncParser:
Args:
string (str): The string to parse.
raise_errors (bool, optional): By default, a failing parse just
raise_errors (bool, optional): By default, a failing parse just
means not parsing the string but leaving it as-is. If this is
`True`, errors (like not closing brackets) will lead to an
ParsingError.
escape (bool, optional): If set, escape all found functions so they
are not executed by later parsing.
strip (bool, optional): If set, strip any inline funcs from string
as if they were not there.
**reserved_kwargs: If given, these are guaranteed to _always_ pass
as part of each parsed callable's **kwargs. These override
same-named default options given in `__init__` as well as any
@ -371,10 +379,18 @@ class FuncParser:
# closing the function list - this means we have a
# ready function-def to run.
open_lparens = 0
infuncstr = self.execute(
curr_func, raise_errors=raise_errors, **reserved_kwargs)
curr_func = None
if strip:
# remove function as if it returned empty
infuncstr = ''
elif escape:
# get function and set it as escaped
infuncstr = escape_char + curr_func.fullstr
else:
# execute the function
infuncstr = self.execute(
curr_func, raise_errors=raise_errors, **reserved_kwargs)
if callstack:
# unnest the higher-level funcdef from stack
# and continue where we were

View file

@ -357,269 +357,173 @@ class ParseStack(list):
self._string_last = False
class InlinefuncError(RuntimeError):
pass
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
"""
Parse the incoming string.
Args:
string (str): The incoming string to parse.
strip (bool, optional): Whether to strip function calls rather than
execute them.
available_funcs (dict, optional): Define an alternative source of functions to parse for.
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
stacktrace (bool, optional): If set, print the stacktrace to log.
Keyword Args:
session (Session): This is sent to this function by Evennia when triggering
it. It is passed to the inlinefunc.
kwargs (any): All other kwargs are also passed on to the inlinefunc.
"""
global _PARSING_CACHE
usecache = False
if not available_funcs:
available_funcs = _INLINE_FUNCS
usecache = True
else:
# make sure the default keys are available, but also allow overriding
tmp = _DEFAULT_FUNCS.copy()
tmp.update(available_funcs)
available_funcs = tmp
if usecache and string in _PARSING_CACHE:
# stack is already cached
stack = _PARSING_CACHE[string]
elif not _RE_STARTTOKEN.search(string):
# if there are no unescaped start tokens at all, return immediately.
return string
else:
# no cached stack; build a new stack and continue
stack = ParseStack()
# process string on stack
ncallable = 0
nlparens = 0
nvalid = 0
if stacktrace:
out = "STRING: {} =>".format(string)
print(out)
logger.log_info(out)
for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict()
if stacktrace:
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
print(out)
logger.log_info(out)
if gdict["singlequote"]:
stack.append(gdict["singlequote"])
elif gdict["doublequote"]:
stack.append(gdict["doublequote"])
elif gdict["leftparens"]:
# we have a left-parens inside a callable
if ncallable:
nlparens += 1
stack.append("(")
elif gdict["end"]:
if nlparens > 0:
nlparens -= 1
stack.append(")")
continue
if ncallable <= 0:
stack.append(")")
continue
args = []
while stack:
operation = stack.pop()
if callable(operation):
if not strip:
stack.append((operation, [arg for arg in reversed(args)]))
ncallable -= 1
break
else:
args.append(operation)
elif gdict["start"]:
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
try:
# try to fetch the matching inlinefunc from storage
stack.append(available_funcs[funcname])
nvalid += 1
except KeyError:
stack.append(available_funcs["nomatch"])
stack.append(funcname)
stack.append(None)
ncallable += 1
elif gdict["escaped"]:
# escaped tokens
token = gdict["escaped"].lstrip("\\")
stack.append(token)
elif gdict["comma"]:
if ncallable > 0:
# commas outside strings and inside a callable are
# used to mark argument separation - we use None
# in the stack to indicate such a separation.
stack.append(None)
else:
# no callable active - just a string
stack.append(",")
else:
# the rest
stack.append(gdict["rest"])
if ncallable > 0:
# this means not all inlinefuncs were complete
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# if stack is larger than limit, throw away parsing
return string + available_funcs["stackfull"](*args, **kwargs)
elif usecache:
# cache the stack - we do this also if we don't check the cache above
_PARSING_CACHE[string] = stack
# run the stack recursively
def _run_stack(item, depth=0):
retval = item
if isinstance(item, tuple):
if strip:
return ""
else:
func, arglist = item
args = [""]
for arg in arglist:
if arg is None:
# an argument-separating comma - start a new arg
args.append("")
else:
# all other args should merge into one string
args[-1] += _run_stack(arg, depth=depth + 1)
# execute the inlinefunc at this point or strip it.
kwargs["inlinefunc_stack_depth"] = depth
retval = "" if strip else func(*args, **kwargs)
return utils.to_str(retval)
retval = "".join(_run_stack(item) for item in stack)
if stacktrace:
out = "STACK: \n{} => {}\n".format(stack, retval)
print(out)
logger.log_info(out)
# execute the stack
return retval
def raw(string):
"""
Escape all inlinefuncs in a string so they won't get parsed.
Args:
string (str): String with inlinefuncs to escape.
"""
def _escape(match):
return "\\" + match.group(0)
return _RE_STARTTOKEN.sub(_escape, string)
# class InlinefuncError(RuntimeError):
# pass
#
# Nick templating
#
"""
This supports the use of replacement templates in nicks:
This happens in two steps:
1) The user supplies a template that is converted to a regex according
to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given.
@desc $1 $2 $3
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
? matches any single character ->
[seq] matches any entry in sequence
[!seq] matches entries not in sequence
Custom arg markers
$N argument position (1-99)
"""
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
"""
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition.
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
template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
evennia.utils.inlinefuncs.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)
n_inargs = len(_RE_NICK_ARG.findall(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)
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs:
raise NickTemplateInvalid
return re.compile(regex_string), template_string
def parse_nick_template(string, template_regex, outtemplate):
"""
Parse a text using a template and map it to another template
Args:
string (str): The input string to processj
template_regex (regex): A template regex created with
initialize_nick_template.
outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2,
etc to match the regex.
"""
match = template_regex.match(string)
if match:
return outtemplate.format(**match.groupdict())
return string
# def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
# """
# Parse the incoming string.
#
# Args:
# string (str): The incoming string to parse.
# strip (bool, optional): Whether to strip function calls rather than
# execute them.
# available_funcs (dict, optional): Define an alternative source of functions to parse for.
# If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
# stacktrace (bool, optional): If set, print the stacktrace to log.
# Keyword Args:
# session (Session): This is sent to this function by Evennia when triggering
# it. It is passed to the inlinefunc.
# kwargs (any): All other kwargs are also passed on to the inlinefunc.
#
#
# """
# global _PARSING_CACHE
# usecache = False
# if not available_funcs:
# available_funcs = _INLINE_FUNCS
# usecache = True
# else:
# # make sure the default keys are available, but also allow overriding
# tmp = _DEFAULT_FUNCS.copy()
# tmp.update(available_funcs)
# available_funcs = tmp
#
# if usecache and string in _PARSING_CACHE:
# # stack is already cached
# stack = _PARSING_CACHE[string]
# elif not _RE_STARTTOKEN.search(string):
# # if there are no unescaped start tokens at all, return immediately.
# return string
# else:
# # no cached stack; build a new stack and continue
# stack = ParseStack()
#
# # process string on stack
# ncallable = 0
# nlparens = 0
# nvalid = 0
#
# if stacktrace:
# out = "STRING: {} =>".format(string)
# print(out)
# logger.log_info(out)
#
# for match in _RE_TOKEN.finditer(string):
# gdict = match.groupdict()
#
# if stacktrace:
# out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
# print(out)
# logger.log_info(out)
#
# if gdict["singlequote"]:
# stack.append(gdict["singlequote"])
# elif gdict["doublequote"]:
# stack.append(gdict["doublequote"])
# elif gdict["leftparens"]:
# # we have a left-parens inside a callable
# if ncallable:
# nlparens += 1
# stack.append("(")
# elif gdict["end"]:
# if nlparens > 0:
# nlparens -= 1
# stack.append(")")
# continue
# if ncallable <= 0:
# stack.append(")")
# continue
# args = []
# while stack:
# operation = stack.pop()
# if callable(operation):
# if not strip:
# stack.append((operation, [arg for arg in reversed(args)]))
# ncallable -= 1
# break
# else:
# args.append(operation)
# elif gdict["start"]:
# funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
# try:
# # try to fetch the matching inlinefunc from storage
# stack.append(available_funcs[funcname])
# nvalid += 1
# except KeyError:
# stack.append(available_funcs["nomatch"])
# stack.append(funcname)
# stack.append(None)
# ncallable += 1
# elif gdict["escaped"]:
# # escaped tokens
# token = gdict["escaped"].lstrip("\\")
# stack.append(token)
# elif gdict["comma"]:
# if ncallable > 0:
# # commas outside strings and inside a callable are
# # used to mark argument separation - we use None
# # in the stack to indicate such a separation.
# stack.append(None)
# else:
# # no callable active - just a string
# stack.append(",")
# else:
# # the rest
# stack.append(gdict["rest"])
#
# if ncallable > 0:
# # this means not all inlinefuncs were complete
# return string
#
# if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# # if stack is larger than limit, throw away parsing
# return string + available_funcs["stackfull"](*args, **kwargs)
# elif usecache:
# # cache the stack - we do this also if we don't check the cache above
# _PARSING_CACHE[string] = stack
#
# # run the stack recursively
# def _run_stack(item, depth=0):
# retval = item
# if isinstance(item, tuple):
# if strip:
# return ""
# else:
# func, arglist = item
# args = [""]
# for arg in arglist:
# if arg is None:
# # an argument-separating comma - start a new arg
# args.append("")
# else:
# # all other args should merge into one string
# args[-1] += _run_stack(arg, depth=depth + 1)
# # execute the inlinefunc at this point or strip it.
# kwargs["inlinefunc_stack_depth"] = depth
# retval = "" if strip else func(*args, **kwargs)
# return utils.to_str(retval)
#
# retval = "".join(_run_stack(item) for item in stack)
# if stacktrace:
# out = "STACK: \n{} => {}\n".format(stack, retval)
# print(out)
# logger.log_info(out)
#
# # execute the stack
# return retval
#
#
# def raw(string):
# """
# Escape all inlinefuncs in a string so they won't get parsed.
#
# Args:
# string (str): String with inlinefuncs to escape.
# """
#
# def _escape(match):
# return "\\" + match.group(0)
#
# return _RE_STARTTOKEN.sub(_escape, string)

View file

@ -7,7 +7,7 @@ Test the funcparser module.
import time
from simpleeval import simple_eval
from parameterized import parameterized
from django.test import TestCase
from django.test import TestCase, override_settings
from evennia.utils import funcparser
@ -135,22 +135,44 @@ class TestFuncParser(TestCase):
Test parsing of string.
"""
t0 = time.time()
#t0 = time.time()
# from evennia import set_trace;set_trace()
ret = self.parser.parse(string)
t1 = time.time()
print(f"time: {(t1-t0)*1000} ms")
#t1 = time.time()
#print(f"time: {(t1-t0)*1000} ms")
self.assertEqual(expected, ret)
def test_parse_raise(self):
"""
Make sure error is raised if told to do so.
"""
string = "Test invalid $dummy()"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, raise_errors=True)
def test_parse_strip(self):
"""
Test the parser's strip functionality.
"""
string = "Test $foo(a,b, $bar()) and $repl($eval(3+2)) things"
ret = self.parser.parse(string, strip=True)
self.assertEqual("Test and things", ret)
def test_parse_escape(self):
"""
Test the parser's escape functionality.
"""
string = "Test $foo(a) and $bar() and $rep(c) things"
ret = self.parser.parse(string, escape=True)
self.assertEqual("Test \$foo(a) and \$bar() and \$rep(c) things", ret)
def test_kwargs_overrides(self):
"""
Test so default kwargs are added and overridden properly
"""
# default kwargs passed on initializations
parser = funcparser.FuncParser(
@ -174,3 +196,40 @@ class TestFuncParser(TestCase):
ret = parser.parse("This is a $foo(foo=moo) string", foo="bar")
self.assertEqual("This is a _test(test=foo, foo=bar) string", ret)
class TestDefaultCallables(TestCase):
"""
Test default callables
"""
@override_settings(INLINEFUNC_MODULES=["evennia.prototypes.protfuncs",
"evennia.utils.inlinefuncs"])
def setUp(self):
from django.conf import settings
self.parser = funcparser.FuncParser(settings.INLINEFUNC_MODULES)
@parameterized.expand([
("Test $pad(Hello, 20, c, -) there", "Test -------Hello-------- there"),
("Test $crop(This is a long test, 12)", "Test This is[...]"),
("Some $space(10) here", "Some here"),
("Some $clr(b, blue color) now", "Some |bblue color|n now"),
("Some $add(1, 2) things", "Some 3 things"),
("Some $sub(10, 2) things", "Some 8 things"),
("Some $mult(3, 2) things", "Some 6 things"),
("Some $div(6, 2) things", "Some 3.0 things"),
("Some $toint(6) things", "Some 6 things"),
])
def test_callable(self, string, expected):
"""
Test default callables.
"""
ret = self.parser.parse(string, raise_errors=True)
self.assertEqual(expected, ret)
def test_random(self):
string = "$random(1,10)"
ret = self.parser.parse(string, raise_errors=True)
ret = int(ret)
self.assertTrue(1 <= ret <= 10)

View file

@ -3,10 +3,10 @@ Unit tests for all sorts of inline text-tag parsing, like ANSI, html conversion,
"""
import re
from django.test import TestCase
from django.test import TestCase, override_settings
from evennia.utils.ansi import ANSIString
from evennia.utils.text2html import TextToHTMLparser
from evennia.utils import inlinefuncs
from evennia.utils import funcparser
class ANSIStringTestCase(TestCase):
@ -352,27 +352,33 @@ class TestTextToHTMLparser(TestCase):
class TestInlineFuncs(TestCase):
"""Test the nested inlinefunc module"""
@override_settings(INLINEFUNC_MODULES=["evennia.utils.inlinefuncs"])
def setUp(self):
from django.conf import settings
self.parser = funcparser.FuncParser(settings.INLINEFUNC_MODULES)
def test_nofunc(self):
self.assertEqual(
inlinefuncs.parse_inlinefunc("as$382ewrw w we w werw,|44943}"),
self.parser.parse("as$382ewrw w we w werw,|44943}"),
"as$382ewrw w we w werw,|44943}",
)
def test_incomplete(self):
self.assertEqual(
inlinefuncs.parse_inlinefunc("testing $blah{without an ending."),
self.parser.parse("testing $blah{without an ending."),
"testing $blah{without an ending.",
)
def test_single_func(self):
self.assertEqual(
inlinefuncs.parse_inlinefunc("this is a test with $pad(centered, 20) text in it."),
self.parser.parse("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(
inlinefuncs.parse_inlinefunc(
self.parser.parse(
"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",
@ -380,16 +386,16 @@ class TestInlineFuncs(TestCase):
def test_escaped(self):
self.assertEqual(
inlinefuncs.parse_inlinefunc(
self.parser.parse(
"this should be $pad('''escaped,''' and '''instead,''' cropped $crop(with a long,5) text., 80)"
),
"this should be escaped, and instead, cropped with text. ",
"this should be '''escaped,''' and '''instead,''' cropped with text. ",
)
def test_escaped2(self):
self.assertEqual(
inlinefuncs.parse_inlinefunc(
self.parser.parse(
'this should be $pad("""escaped,""" and """instead,""" cropped $crop(with a long,5) text., 80)'
),
"this should be escaped, and instead, cropped with text. ",
"this should be \"\"\"escaped,\"\"\" and \"\"\"instead,\"\"\" cropped with text. ",
)

View file

@ -385,30 +385,3 @@ class TestPercent(TestCase):
self.assertEqual(utils.percent(3, 1, 1), "0.0%")
self.assertEqual(utils.percent(3, 0, 1), "100.0%")
self.assertEqual(utils.percent(-3, 0, 1), "0.0%")
class ParseArgumentsTest(TestCase):
def _run_test(s):
return utils.parse_arguments(s)
def test_happy_flow(self):
s = "1, \"The text \\\"Hello, world.\\\" is often used by programmers to test if their code works.\", caller, looker=\"Qwerty\""
args, kwargs = ParseArgumentsTest._run_test(s)
self.assertEqual(len(args), 3)
self.assertEqual(args[0], 1)
self.assertEqual(args[1], "The text \"Hello, world.\" is often used by programmers to test if their code works.")
#self.assertEqual(args[2], "caller")
self.assertEqual(len(kwargs), 1)
self.assertEqual(kwargs["looker"], "Qwerty")
def test_malformed_string(self):
s = ",(,),"
args, kwargs = ParseArgumentsTest._run_test(s)
self.assertEqual(len(args), 4)
self.assertEqual(args[0], "")
self.assertEqual(args[1].__class__, utils.FunctionArgument)
self.assertEqual(args[1].name, "(")
self.assertEqual(args[2].__class__, utils.FunctionArgument)
self.assertEqual(args[2].name, ")")
self.assertEqual(args[3], "")
self.assertEqual(len(kwargs), 0)