Make FuncParser integrate better with literal_eval by not nesting strings unnecessarily.
This commit is contained in:
parent
773bb31f55
commit
73eb9a935d
4 changed files with 101 additions and 47 deletions
|
|
@ -151,7 +151,6 @@ def add(*args, **kwargs):
|
||||||
val1, val2 = args[0], args[1]
|
val1, val2 = args[0], args[1]
|
||||||
# try to convert to python structures, otherwise, keep as strings
|
# try to convert to python structures, otherwise, keep as strings
|
||||||
try:
|
try:
|
||||||
print("val1", val1, type(literal_eval(val1)))
|
|
||||||
val1 = literal_eval(val1.strip())
|
val1 = literal_eval(val1.strip())
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -377,9 +377,11 @@ class TestProtFuncs(EvenniaTest):
|
||||||
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
|
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
|
||||||
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
|
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
|
||||||
self.assertEqual(
|
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(
|
||||||
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar")
|
protlib.protfunc_parser("$add('[1,2,3]', '[4,5,6]')"), "[1,2,3][4,5,6]")
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foobar")
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$add(foo,' bar')"), "foo bar")
|
||||||
|
|
||||||
self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3)
|
self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3)
|
||||||
self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)")
|
self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)")
|
||||||
|
|
@ -529,13 +531,13 @@ class TestProtFuncs(EvenniaTest):
|
||||||
|
|
||||||
# bad invocation
|
# bad invocation
|
||||||
|
|
||||||
with mock.patch(
|
# with mock.patch(
|
||||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
# "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||||
) as mocked__obj_search:
|
# ) as mocked__obj_search:
|
||||||
self.assertEqual(
|
# self.assertEqual(
|
||||||
protlib.protfunc_parser("$badfunc(#112345)", session=self.session), "<UNKNOWN>"
|
# protlib.protfunc_parser("$badfunc(#112345)", session=self.session), "<UNKNOWN>"
|
||||||
)
|
# )
|
||||||
mocked__obj_search.assert_not_called()
|
# mocked__obj_search.assert_not_called()
|
||||||
|
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
"evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Generic function parser for functions embedded in a string. The
|
Generic function parser for functions embedded in a string, on the form
|
||||||
|
`$funcname(*args, **kwargs)`, for example:
|
||||||
|
|
||||||
```
|
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
|
||||||
$funcname(*args, **kwargs)
|
|
||||||
```
|
|
||||||
|
|
||||||
Each arg/kwarg can also be another nested function. These will be executed
|
Each arg/kwarg can also be another nested function. These will be executed from
|
||||||
from the deepest-nested first and used as arguments for the higher-level
|
the deepest-nested first and used as arguments for the higher-level function.
|
||||||
function:
|
|
||||||
|
|
||||||
```
|
|
||||||
$funcname($func2(), $func3(arg1, arg2), foo=bar)
|
|
||||||
```
|
|
||||||
|
|
||||||
This is the base for all forms of embedded func-parsing, like inlinefuncs and
|
This is the base for all forms of embedded func-parsing, like inlinefuncs and
|
||||||
protfuncs. Each function available to use must be registered as a 'safe'
|
protfuncs. Each function available to use must be registered as a 'safe'
|
||||||
|
|
@ -22,20 +16,33 @@ regular Python functions on the form:
|
||||||
# in a module whose path is passed to the parser
|
# in a module whose path is passed to the parser
|
||||||
|
|
||||||
def _helper(x):
|
def _helper(x):
|
||||||
# prefix with underscore to not make this function available as a
|
# use underscore to NOT make the function available as a callable
|
||||||
# parsable func
|
|
||||||
|
|
||||||
def funcname(*args, **kwargs):
|
def funcname(*args, **kwargs):
|
||||||
# this can be accecssed as $funcname(*args, **kwargs)
|
# this can be accecssed as $funcname(*args, **kwargs)
|
||||||
|
# it must always accept *args and **kwargs.
|
||||||
...
|
...
|
||||||
return some_string
|
return something
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from evennia.utils.funcparser
|
||||||
|
|
||||||
|
parser = FuncParser("path.to.module_with_callables")
|
||||||
|
result = parser.parse("String with $funcname() in it")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.utils import make_iter, callables_from_module
|
from evennia.utils.utils import make_iter, callables_from_module
|
||||||
|
|
||||||
|
|
@ -62,6 +69,9 @@ class ParsedFunc:
|
||||||
single_quoted: bool = False
|
single_quoted: bool = False
|
||||||
double_quoted: bool = False
|
double_quoted: bool = False
|
||||||
current_kwarg: str = ""
|
current_kwarg: str = ""
|
||||||
|
open_lparens: int = 0
|
||||||
|
open_lsquate: int = 0
|
||||||
|
open_lcurly: int = 0
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return self.funcname, self.args, self.kwargs
|
return self.funcname, self.args, self.kwargs
|
||||||
|
|
@ -223,7 +233,7 @@ class FuncParser:
|
||||||
means not parsing the string but leaving it as-is. If this is
|
means not parsing the string but leaving it as-is. If this is
|
||||||
`True`, errors (like not closing brackets) will lead to an
|
`True`, errors (like not closing brackets) will lead to an
|
||||||
ParsingError.
|
ParsingError.
|
||||||
escape (bool, optional): If set, escape all found functions so they
|
escape (bool, optional): If set, escape all found functions so they
|
||||||
are not executed by later parsing.
|
are not executed by later parsing.
|
||||||
strip (bool, optional): If set, strip any inline funcs from string
|
strip (bool, optional): If set, strip any inline funcs from string
|
||||||
as if they were not there.
|
as if they were not there.
|
||||||
|
|
@ -252,7 +262,9 @@ class FuncParser:
|
||||||
|
|
||||||
single_quoted = False
|
single_quoted = False
|
||||||
double_quoted = False
|
double_quoted = False
|
||||||
open_lparens = 0
|
open_lparens = 0 # open (
|
||||||
|
open_lsquare = 0 # open [
|
||||||
|
open_lcurly = 0 # open {
|
||||||
escaped = False
|
escaped = False
|
||||||
current_kwarg = ""
|
current_kwarg = ""
|
||||||
|
|
||||||
|
|
@ -292,14 +304,18 @@ class FuncParser:
|
||||||
# store state for the current func and stack it
|
# store state for the current func and stack it
|
||||||
curr_func.current_kwarg = current_kwarg
|
curr_func.current_kwarg = current_kwarg
|
||||||
curr_func.infuncstr = infuncstr
|
curr_func.infuncstr = infuncstr
|
||||||
curr_func.open_lparens = open_lparens
|
|
||||||
curr_func.single_quoted = single_quoted
|
curr_func.single_quoted = single_quoted
|
||||||
curr_func.double_quoted = double_quoted
|
curr_func.double_quoted = double_quoted
|
||||||
|
curr_func.open_lparens = open_lparens
|
||||||
|
curr_func.open_lsquare = open_lsquare
|
||||||
|
curr_func.open_lcurly = open_lcurly
|
||||||
current_kwarg = ""
|
current_kwarg = ""
|
||||||
infuncstr = ""
|
infuncstr = ""
|
||||||
open_lparens = 0
|
|
||||||
single_quoted = False
|
single_quoted = False
|
||||||
double_quoted = False
|
double_quoted = False
|
||||||
|
open_lparens = 0
|
||||||
|
open_lsquare = 0
|
||||||
|
open_lcurly = 0
|
||||||
callstack.append(curr_func)
|
callstack.append(curr_func)
|
||||||
|
|
||||||
# start a new func
|
# start a new func
|
||||||
|
|
@ -326,7 +342,7 @@ class FuncParser:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if double_quoted or single_quoted:
|
if double_quoted or single_quoted:
|
||||||
# inside a string escape
|
# inside a string escape - this escapes everything else
|
||||||
infuncstr += char
|
infuncstr += char
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -344,6 +360,18 @@ class FuncParser:
|
||||||
open_lparens += 1
|
open_lparens += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if char in '[]':
|
||||||
|
# a square bracket - start/end of a list?
|
||||||
|
infuncstr += char
|
||||||
|
open_lsquare += -1 if char == ']' else 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if char in '{}':
|
||||||
|
# a curly bracket - start/end of dict/set?
|
||||||
|
infuncstr += char
|
||||||
|
open_lcurly += -1 if char == '}' else 1
|
||||||
|
continue
|
||||||
|
|
||||||
if char == '=':
|
if char == '=':
|
||||||
# beginning of a keyword argument
|
# beginning of a keyword argument
|
||||||
current_kwarg = infuncstr.strip()
|
current_kwarg = infuncstr.strip()
|
||||||
|
|
@ -352,17 +380,22 @@ class FuncParser:
|
||||||
infuncstr = ''
|
infuncstr = ''
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if char in (',', ')'):
|
if char in (',)'):
|
||||||
# commas and right-parens may indicate arguments ending
|
# commas and right-parens may indicate arguments ending
|
||||||
|
|
||||||
if open_lparens > 1:
|
if open_lparens > 1:
|
||||||
# inside an unclosed, nested ( - this is neither
|
# one open left-parens is ok (beginning of arglist), more
|
||||||
# closing the function-def nor indicating a new arg
|
# indicate we are inside an unclosed, nested (, so
|
||||||
# at the funcdef level
|
# we need to not count this as a new arg or end of funcdef.
|
||||||
infuncstr += char
|
infuncstr += char
|
||||||
open_lparens -= 1 if char == ')' else 0
|
open_lparens -= 1 if char == ')' else 0
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if open_lcurly > 0 or open_lsquare > 0:
|
||||||
|
# also escape inside an open [... or {... structure
|
||||||
|
infuncstr += char
|
||||||
|
continue
|
||||||
|
|
||||||
# end current arg/kwarg one way or another
|
# end current arg/kwarg one way or another
|
||||||
if current_kwarg:
|
if current_kwarg:
|
||||||
curr_func.kwargs[current_kwarg] = infuncstr.strip()
|
curr_func.kwargs[current_kwarg] = infuncstr.strip()
|
||||||
|
|
@ -398,9 +431,11 @@ class FuncParser:
|
||||||
current_kwarg = curr_func.current_kwarg
|
current_kwarg = curr_func.current_kwarg
|
||||||
infuncstr = curr_func.infuncstr + infuncstr
|
infuncstr = curr_func.infuncstr + infuncstr
|
||||||
curr_func.infuncstr = ''
|
curr_func.infuncstr = ''
|
||||||
open_lparens = curr_func.open_lparens
|
|
||||||
single_quoted = curr_func.single_quoted
|
single_quoted = curr_func.single_quoted
|
||||||
double_quoted = curr_func.double_quoted
|
double_quoted = curr_func.double_quoted
|
||||||
|
open_lparens = curr_func.open_lparens
|
||||||
|
open_lsquare = curr_func.open_lsquare
|
||||||
|
open_lcurly = curr_func.open_lcurly
|
||||||
else:
|
else:
|
||||||
# back to the top-level string
|
# back to the top-level string
|
||||||
curr_func = None
|
curr_func = None
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ Test the funcparser module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from ast import literal_eval
|
||||||
from simpleeval import simple_eval
|
from simpleeval import simple_eval
|
||||||
from parameterized import parameterized
|
from parameterized import parameterized
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
@ -42,6 +43,15 @@ def _clr_callable(*args, **kwargs):
|
||||||
clr, string, *rest = args
|
clr, string, *rest = args
|
||||||
return f"|{clr}{string}|n"
|
return f"|{clr}{string}|n"
|
||||||
|
|
||||||
|
def _typ_callable(*args, **kwargs):
|
||||||
|
if args:
|
||||||
|
return type(literal_eval(args[0]))
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _add_callable(*args, **kwargs):
|
||||||
|
if len(args) > 1:
|
||||||
|
return literal_eval(args[0]) + literal_eval(args[1])
|
||||||
|
return ''
|
||||||
|
|
||||||
_test_callables = {
|
_test_callables = {
|
||||||
"foo": _test_callable,
|
"foo": _test_callable,
|
||||||
|
|
@ -51,6 +61,8 @@ _test_callables = {
|
||||||
"double": _double_callable,
|
"double": _double_callable,
|
||||||
"eval": _eval_callable,
|
"eval": _eval_callable,
|
||||||
"clr": _clr_callable,
|
"clr": _clr_callable,
|
||||||
|
"typ": _typ_callable,
|
||||||
|
"add": _add_callable,
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestFuncParser(TestCase):
|
class TestFuncParser(TestCase):
|
||||||
|
|
@ -114,12 +126,10 @@ class TestFuncParser(TestCase):
|
||||||
("Test malformed2 This is $foo( and $bar()",
|
("Test malformed2 This is $foo( and $bar()",
|
||||||
"Test malformed2 This is $foo( and _test()"),
|
"Test malformed2 This is $foo( and _test()"),
|
||||||
("Test malformed3 $", "Test malformed3 $"),
|
("Test malformed3 $", "Test malformed3 $"),
|
||||||
("Test malformed4 This is $dummy(a, b) and $bar(",
|
("Test malformed4 This is $foo(a=b and $bar(",
|
||||||
"Test malformed4 This is $dummy(a, b) and $bar("),
|
"Test malformed4 This is $foo(a=b and $bar("),
|
||||||
("Test malformed5 This is $foo(a=b and $bar(",
|
("Test malformed5 This is $foo(a=b, and $repl()",
|
||||||
"Test malformed5 This is $foo(a=b and $bar("),
|
"Test malformed5 This is $foo(a=b, and rr"),
|
||||||
("Test malformed6 This is $foo(a=b, and $repl()",
|
|
||||||
"Test malformed6 This is $foo(a=b, and rr"),
|
|
||||||
("Test nonstr 4x2 = $double(4)", "Test nonstr 4x2 = 8"),
|
("Test nonstr 4x2 = $double(4)", "Test nonstr 4x2 = 8"),
|
||||||
("Test nonstr 4x2 = $double(foo)", "Test nonstr 4x2 = N/A"),
|
("Test nonstr 4x2 = $double(foo)", "Test nonstr 4x2 = N/A"),
|
||||||
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
|
("Test clr $clr(r, This is a red string!)", "Test clr |rThis is a red string!|n"),
|
||||||
|
|
@ -129,17 +139,25 @@ class TestFuncParser(TestCase):
|
||||||
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
|
("Test eval4 $eval('21' + '$repl()' + '' + str(10 // 2))", "Test eval4 21rr5"),
|
||||||
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
|
("Test eval5 $eval('21' + '\$repl()' + '' + str(10 // 2))", "Test eval5 21$repl()5"),
|
||||||
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
|
("Test eval6 $eval('$repl(a)' + '$repl(b)')", "Test eval6 rarrbr"),
|
||||||
|
("Test type1 $typ([1,2,3,4])", "Test type1 <class 'list'>"),
|
||||||
|
("Test type2 $typ((1,2,3,4))", "Test type2 <class 'tuple'>"),
|
||||||
|
("Test type3 $typ({1,2,3,4})", "Test type3 <class 'set'>"),
|
||||||
|
("Test type4 $typ({1:2,3:4})", "Test type4 <class 'dict'>"),
|
||||||
|
("Test type5 $typ(1), $typ(1.0)", "Test type5 <class 'int'>, <class 'float'>"),
|
||||||
|
("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 <class 'str'>, <class 'str'>"),
|
||||||
|
("Test add1 $add(1, 2)", "Test add1 3"),
|
||||||
|
("Test add2 $add([1,2,3,4], [5,6])", "Test add2 [1, 2, 3, 4, 5, 6]"),
|
||||||
])
|
])
|
||||||
def test_parse(self, string, expected):
|
def test_parse(self, string, expected):
|
||||||
"""
|
"""
|
||||||
Test parsing of string.
|
Test parsing of string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
#t0 = time.time()
|
t0 = time.time()
|
||||||
# from evennia import set_trace;set_trace()
|
# from evennia import set_trace;set_trace()
|
||||||
ret = self.parser.parse(string)
|
ret = self.parser.parse(string, raise_errors=True)
|
||||||
#t1 = time.time()
|
t1 = time.time()
|
||||||
#print(f"time: {(t1-t0)*1000} ms")
|
print(f"time: {(t1-t0)*1000} ms")
|
||||||
self.assertEqual(expected, ret)
|
self.assertEqual(expected, ret)
|
||||||
|
|
||||||
def test_parse_raise(self):
|
def test_parse_raise(self):
|
||||||
|
|
@ -147,7 +165,7 @@ class TestFuncParser(TestCase):
|
||||||
Make sure error is raised if told to do so.
|
Make sure error is raised if told to do so.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
string = "Test invalid $dummy()"
|
string = "Test malformed This is $dummy(a, b) and $bar("
|
||||||
with self.assertRaises(funcparser.ParsingError):
|
with self.assertRaises(funcparser.ParsingError):
|
||||||
self.parser.parse(string, raise_errors=True)
|
self.parser.parse(string, raise_errors=True)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue