diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 864520a732..1216a679f5 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -72,6 +72,7 @@ class ParsedFunc: open_lparens: int = 0 open_lsquate: int = 0 open_lcurly: int = 0 + exec_return = "" def get(self): return self.funcname, self.args, self.kwargs @@ -214,7 +215,7 @@ class FuncParser: kwargs = {**self.default_kwargs, **kwargs, **reserved_kwargs} try: - return str(func(*args, **kwargs)) + return func(*args, **kwargs) except Exception: logger.log_trace() if raise_errors: @@ -267,6 +268,7 @@ class FuncParser: open_lcurly = 0 # open { escaped = False current_kwarg = "" + exec_return = "" curr_func = None fullstr = '' # final string @@ -316,6 +318,7 @@ class FuncParser: open_lparens = 0 open_lsquare = 0 open_lcurly = 0 + exec_return = "" callstack.append(curr_func) # start a new func @@ -329,6 +332,13 @@ class FuncParser: # in a function def (can be nested) + if exec_return != '' and char not in (",=)"): + # if exec_return is followed by any other character + # than one demarking an arg,kwarg or function-end + # it must immediately merge as a string + infuncstr += str(exec_return) + exec_return = '' + if char == "'": # note that this is the same as "\'" # a single quote - flip status single_quoted = not single_quoted @@ -342,7 +352,7 @@ class FuncParser: continue if double_quoted or single_quoted: - # inside a string escape - this escapes everything else + # inside a string definition - this escapes everything else infuncstr += char continue @@ -374,6 +384,8 @@ class FuncParser: if char == '=': # beginning of a keyword argument + if exec_return != '': + infuncstr = exec_return current_kwarg = infuncstr.strip() curr_func.kwargs[current_kwarg] = "" curr_func.fullstr += infuncstr + char @@ -396,16 +408,28 @@ class FuncParser: infuncstr += char continue - # end current arg/kwarg one way or another - if current_kwarg: - curr_func.kwargs[current_kwarg] = infuncstr.strip() - current_kwarg = "" - elif infuncstr.strip(): - curr_func.args.append(infuncstr.strip()) + if exec_return != '': + # store the execution return as-received + if current_kwarg: + curr_func.kwargs[current_kwarg] = exec_return + else: + curr_func.args.append(exec_return) + else: + # store a string instead + if current_kwarg: + curr_func.kwargs[current_kwarg] = infuncstr.strip() + elif infuncstr.strip(): + # don't store the empty string + curr_func.args.append(infuncstr.strip()) - # we need to store the full string so we can print it 'raw' in - # case this funcdef turns out to e.g. lack an ending paranthesis - curr_func.fullstr += infuncstr + char + # note that at this point either exec_return or infuncstr will + # be empty. We need to store the full string so we can print + # it 'raw' in case this funcdef turns out to e.g. lack an + # ending paranthesis + curr_func.fullstr += str(exec_return) + infuncstr + char + + current_kwarg = "" + exec_return = '' infuncstr = '' if char == ')': @@ -415,13 +439,14 @@ class FuncParser: if strip: # remove function as if it returned empty - infuncstr = '' + exec_return = '' elif escape: # get function and set it as escaped - infuncstr = escape_char + curr_func.fullstr + exec_return = escape_char + curr_func.fullstr else: - # execute the function - infuncstr = self.execute( + # execute the function - the result may be a string or + # something else + exec_return = self.execute( curr_func, raise_errors=raise_errors, **reserved_kwargs) if callstack: @@ -429,7 +454,11 @@ class FuncParser: # and continue where we were curr_func = callstack.pop() current_kwarg = curr_func.current_kwarg - infuncstr = curr_func.infuncstr + infuncstr + if curr_func.infuncstr: + # if we have an ongoing string, we must merge the + # exec into this as a part of that string + infuncstr = curr_func.infuncstr + str(exec_return) + exec_return = '' curr_func.infuncstr = '' single_quoted = curr_func.single_quoted double_quoted = curr_func.double_quoted @@ -437,13 +466,14 @@ class FuncParser: open_lsquare = curr_func.open_lsquare open_lcurly = curr_func.open_lcurly else: - # back to the top-level string + # back to the top-level string - this means the + # exec_return should always be converted to a string. curr_func = None - fullstr += infuncstr + fullstr += str(exec_return) + exec_return = '' infuncstr = '' continue - # no special char infuncstr += char if curr_func: diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index a2c75becdb..52a947e143 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -44,15 +44,27 @@ def _clr_callable(*args, **kwargs): return f"|{clr}{string}|n" def _typ_callable(*args, **kwargs): - if args: - return type(literal_eval(args[0])) - return '' + try: + if isinstance(args[0], str): + return type(literal_eval(args[0])) + else: + return type(args[0]) + except (SyntaxError, ValueError): + return type("") def _add_callable(*args, **kwargs): if len(args) > 1: return literal_eval(args[0]) + literal_eval(args[1]) return '' +def _lit_callable(*args, **kwargs): + return literal_eval(args[0]) + +def _lsum_callable(*args, **kwargs): + if isinstance(args[0], (list, tuple)): + return sum(val for val in args[0]) + return '' + _test_callables = { "foo": _test_callable, "bar": _test_callable, @@ -63,6 +75,8 @@ _test_callables = { "clr": _clr_callable, "typ": _typ_callable, "add": _add_callable, + "lit": _lit_callable, + "sum": _lsum_callable, } class TestFuncParser(TestCase): @@ -147,17 +161,21 @@ class TestFuncParser(TestCase): ("Test type6 $typ('1'), $typ(\"1.0\")", "Test type6 , "), ("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]"), + ("Test literal1 $sum($lit([1,2,3,4,5,6]))", "Test literal1 21"), + ("Test literal2 $typ($lit(1))", "Test literal2 "), + ("Test literal3 $typ($lit(1)aaa)", "Test literal3 "), + ("Test literal4 $typ(aaa$lit(1))", "Test literal4 "), ]) def test_parse(self, string, expected): """ Test parsing of string. """ - t0 = time.time() + # t0 = time.time() # from evennia import set_trace;set_trace() ret = self.parser.parse(string, raise_errors=True) - 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):