diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index f9c5ba62b1..f7566e56b0 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -177,9 +177,7 @@ def _set_property(caller, raw_string, **kwargs): if kwargs.get("test_parse", True): out.append(" Simulating prototype-func parsing ...") - err, parsed_value = protlib.protfunc_parser(value, testing=True) - if err: - out.append(" |yPython `literal_eval` warning: {}|n".format(err)) + parsed_value = protlib.protfunc_parser(value, testing=True, prototype=prototype) if parsed_value != value: out.append( " |g(Example-)value when parsed ({}):|n {}".format(type(parsed_value), parsed_value) @@ -264,7 +262,7 @@ def _validate_prototype(prototype): def _format_protfuncs(): out = [] sorted_funcs = [ - (key, func) for key, func in sorted(protlib.PROT_FUNCS.items(), key=lambda tup: tup[0]) + (key, func) for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0]) ] for protfunc_name, protfunc in sorted_funcs: out.append( diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index c501d888ad..bde784f60e 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -44,7 +44,8 @@ def protfunc_callable_protkey(*args, **kwargs): return "" prototype = kwargs.get("prototype", {}) - return prototype[args[0].strip()] + prot_value = prototype[args[0]] + return funcparser.funcparser_callable_eval(prot_value, **kwargs) # this is picked up by FuncParser diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index 042670ea05..7ed6aba55d 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -59,11 +59,14 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + ( ) PROTOTYPE_TAG_CATEGORY = "from_prototype" _PROTOTYPE_TAG_META_CATEGORY = "db_prototype" -PROT_FUNCS = {} _PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()" +# the protfunc parser +FUNC_PARSER = FuncParser(settings.PROT_FUNC_MODULES) + + class PermissionError(RuntimeError): pass @@ -710,17 +713,6 @@ def validate_prototype( prototype["prototype_locks"] = prototype_locks -# Protfunc parsing (in-prototype functions) - -for mod in settings.PROT_FUNC_MODULES: - try: - callables = callables_from_module(mod) - PROT_FUNCS.update(callables) - except ImportError: - logger.log_trace() - raise - - def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, caller=None, **kwargs): """ Parse a prototype value string for a protfunc and process it. @@ -733,8 +725,6 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F protfuncs, all other types are returned as-is. available_functions (dict, optional): Mapping of name:protfunction to use for this parsing. If not set, use default sources. - testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may - behave differently. stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser. Keyword Args: @@ -746,33 +736,16 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F any (any): Passed on to the protfunc. Returns: - testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is - either None or a string detailing the error from protfunc_parser or seen when trying to - run `literal_eval` on the parsed string. - any (any): A structure to replace the string on the prototype level. If this is a - callable or a (callable, (args,)) structure, it will be executed as if one had supplied - it to the prototype directly. This structure is also passed through literal_eval so one - can get actual Python primitives out of it (not just strings). It will also identify - eventual object #dbrefs in the output from the protfunc. + any: A structure to replace the string on the prototype leve. Note + that FunctionParser functions $funcname(*args, **kwargs) can return any + data type to insert into the prototype. """ if not isinstance(value, str): return value - available_functions = PROT_FUNCS if available_functions is None else available_functions + result = FUNC_PARSER.parse(value, raise_errors=True, return_str=False, caller=caller, **kwargs) - result = FuncParser(available_functions).parse( - value, raise_errors=True, caller=caller, **kwargs) - - err = None - try: - result = literal_eval(result) - except ValueError: - pass - except Exception as exc: - err = str(exc) - if testing: - return err, result return result @@ -787,7 +760,7 @@ def format_available_protfuncs(): clr (str, optional): What coloration tag to use. """ out = [] - for protfunc_name, protfunc in PROT_FUNCS.items(): + for protfunc_name, protfunc in FUNC_PARSER.callables.items(): out.append( "- |c${name}|n - |W{docs}".format( name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "") @@ -912,7 +885,7 @@ def check_permission(prototype_key, action, default=True): return default -def init_spawn_value(value, validator=None): +def init_spawn_value(value, validator=None, caller=None): """ Analyze the prototype value and produce a value useful at the point of spawning. @@ -923,6 +896,8 @@ def init_spawn_value(value, validator=None): other - will be assigned depending on the variable type validator (callable, optional): If given, this will be called with the value to check and guarantee the outcome is of a given type. + caller (Object or Account): This is necessary for certain protfuncs that perform object + searches and have to check permissions. Returns: any (any): The (potentially pre-processed value to use for this prototype key) @@ -937,7 +912,7 @@ def init_spawn_value(value, validator=None): value = validator(value[0](*make_iter(args))) else: value = validator(value) - result = protfunc_parser(value) + result = protfunc_parser(value, caller=caller) if result != value: return validator(result) return result diff --git a/evennia/prototypes/tests.py b/evennia/prototypes/tests.py index fc890aa860..a84c56c39e 100644 --- a/evennia/prototypes/tests.py +++ b/evennia/prototypes/tests.py @@ -338,225 +338,19 @@ class TestProtLib(EvenniaTest): self.assertEqual(match, [self.prot]) -@override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"], CLIENT_DEFAULT_WIDTH=20) class TestProtFuncs(EvenniaTest): - def setUp(self): - super(TestProtFuncs, self).setUp() - self.prot = { - "prototype_key": "test_prototype", - "prototype_desc": "testing prot", - "key": "ExampleObj", - } - - @mock.patch("evennia.prototypes.protfuncs.base_random", new=mock.MagicMock(return_value=0.5)) - @mock.patch("evennia.prototypes.protfuncs.base_randint", new=mock.MagicMock(return_value=5)) - def test_protfuncs(self): - self.assertEqual(protlib.protfunc_parser("$random()"), 0.5) - self.assertEqual(protlib.protfunc_parser("$randint(1, 10)"), 5) - self.assertEqual(protlib.protfunc_parser("$left_justify( foo )"), "foo ") - self.assertEqual(protlib.protfunc_parser("$right_justify( foo )"), " foo") - self.assertEqual(protlib.protfunc_parser("$center_justify(foo )"), " foo ") - self.assertEqual( - protlib.protfunc_parser("$full_justify(foo bar moo too)"), "foo bar moo too" - ) - self.assertEqual( - protlib.protfunc_parser("$right_justify( foo )", testing=True), - ("unexpected indent (, line 1)", " foo"), - ) + @override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"]) + def test_protkey_protfunc(self): test_prot = {"key1": "value1", "key2": 2} self.assertEqual( protlib.protfunc_parser("$protkey(key1)", testing=True, prototype=test_prot), - (None, "value1"), + "value1", ) self.assertEqual( - protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), (None, 2) - ) - - 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]) - self.assertEqual( - 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.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)") - - self.assertEqual(protlib.protfunc_parser("$mult(5, 2)"), 10) - self.assertEqual(protlib.protfunc_parser("$mult( 5 , 10)"), 50) - self.assertEqual(protlib.protfunc_parser("$mult('foo',3)"), "foofoofoo") - self.assertEqual(protlib.protfunc_parser("$mult(foo,3)"), "foofoofoo") - self.assertRaises(TypeError, protlib.protfunc_parser, "$mult(foo, foo)") - - self.assertEqual(protlib.protfunc_parser("$toint(5.3)"), 5) - - self.assertEqual(protlib.protfunc_parser("$div(5, 2)"), 2.5) - self.assertEqual(protlib.protfunc_parser("$toint($div(5, 2))"), 2) - self.assertEqual(protlib.protfunc_parser("$sub($add(5, 3), $add(10, 2))"), -4) - - self.assertEqual(protlib.protfunc_parser("$eval('2')"), "2") - - self.assertEqual( - protlib.protfunc_parser("$eval(['test', 1, '2', 3.5, \"foo\"])"), - ["test", 1, "2", 3.5, "foo"], - ) - self.assertEqual( - protlib.protfunc_parser("$eval({'test': '1', 2:3, 3: $toint(3.5)})"), - {"test": "1", 2: 3, 3: 3}, - ) - - # no object search - odbref = self.obj1.dbref - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("obj({})".format(odbref), session=self.session), - "obj({})".format(odbref), - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("dbref({})".format(odbref), session=self.session), - "dbref({})".format(odbref), - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("stone(#12345)", session=self.session), "stone(#12345)" - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual(protlib.protfunc_parser(odbref, session=self.session), odbref) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual(protlib.protfunc_parser("#12345", session=self.session), "#12345") - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("nothing({})".format(odbref), session=self.session), - "nothing({})".format(odbref), - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual(protlib.protfunc_parser("(#12345)", session=self.session), "(#12345)") - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("obj(Char)", session=self.session), "obj(Char)" - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("objlist({})".format(odbref), session=self.session), - "objlist({})".format(odbref), - ) - mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("dbref(Char)", session=self.session), "dbref(Char)" - ) - mocked__obj_search.assert_not_called() - - # obj search happens - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("$objlist({})".format(odbref), session=self.session), - [odbref], - ) - mocked__obj_search.assert_called_once() - assert (odbref,) == mocked__obj_search.call_args[0] - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("$obj({})".format(odbref), session=self.session), odbref - ) - mocked__obj_search.assert_called_once() - assert (odbref,) == mocked__obj_search.call_args[0] - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual( - protlib.protfunc_parser("$dbref({})".format(odbref), session=self.session), odbref - ) - mocked__obj_search.assert_called_once() - assert (odbref,) == mocked__obj_search.call_args[0] - - cdbref = self.char1.dbref - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), cdbref) - mocked__obj_search.assert_called_once() - assert ("Char",) == mocked__obj_search.call_args[0] - - # bad invocation - - # with mock.patch( - # "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - # ) as mocked__obj_search: - # self.assertEqual( - # protlib.protfunc_parser("$badfunc(#112345)", session=self.session), "" - # ) - # mocked__obj_search.assert_not_called() - - with mock.patch( - "evennia.prototypes.protfuncs._obj_search", wraps=protofuncs._obj_search - ) as mocked__obj_search: - self.assertRaises(ValueError, protlib.protfunc_parser, "$dbref(Char)") - mocked__obj_search.assert_not_called() - - self.assertEqual( - protlib.value_to_obj(protlib.protfunc_parser(cdbref, session=self.session)), self.char1 - ) - self.assertEqual( - protlib.value_to_obj_or_any(protlib.protfunc_parser(cdbref, session=self.session)), - self.char1, - ) - self.assertEqual( - protlib.value_to_obj_or_any( - protlib.protfunc_parser("[1,2,3,'{}',5]".format(cdbref), session=self.session) - ), - [1, 2, 3, self.char1, 5], + protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), + 2 ) diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 3fec8f59d5..315c70bc7c 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -280,10 +280,11 @@ class TestDefaultCallables(TestCase): Test default callables. """ - @override_settings(INLINEFUNC_MODULES=["evennia.utils.funcparser"]) def setUp(self): from django.conf import settings - self.parser = funcparser.FuncParser(settings.INLINEFUNC_MODULES) + self.parser = funcparser.FuncParser({**funcparser.FUNCPARSER_CALLABLES, + **funcparser.ACTOR_STANCE_CALLABLES}) + self.obj1 = _DummyObj("Char1") self.obj2 = _DummyObj("Char2") @@ -349,18 +350,57 @@ class TestDefaultCallables(TestCase): ret = int(ret) self.assertTrue(1 <= ret <= 10) + def test_nofunc(self): + self.assertEqual( + self.parser.parse("as$382ewrw w we w werw,|44943}"), + "as$382ewrw w we w werw,|44943}", + ) + + def test_incomplete(self): + self.assertEqual( + self.parser.parse("testing $blah{without an ending."), + "testing $blah{without an ending.", + ) + + def test_single_func(self): + self.assertEqual( + 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( + 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", + ) + + def test_escaped(self): + self.assertEqual( + 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. ", + ) + + def test_escaped2(self): + self.assertEqual( + 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. ", + ) + class TestCallableSearch(test_resources.EvenniaTest): """ Test the $search(query) callable """ - @override_settings(INLINEFUNC_MODULES=["evennia.utils.funcparser"]) def setUp(self): super().setUp() - - from django.conf import settings - self.parser = funcparser.FuncParser(settings.INLINEFUNC_MODULES) + self.parser = funcparser.FuncParser(funcparser.SEARCHING_CALLABLES) def test_search_obj(self): """ diff --git a/evennia/utils/tests/test_tagparsing.py b/evennia/utils/tests/test_tagparsing.py index 8272a8eaae..5c47bc5b6a 100644 --- a/evennia/utils/tests/test_tagparsing.py +++ b/evennia/utils/tests/test_tagparsing.py @@ -347,55 +347,3 @@ class TestTextToHTMLparser(TestCase): '' 'http://example.com/', ) - - -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( - self.parser.parse("as$382ewrw w we w werw,|44943}"), - "as$382ewrw w we w werw,|44943}", - ) - - def test_incomplete(self): - self.assertEqual( - self.parser.parse("testing $blah{without an ending."), - "testing $blah{without an ending.", - ) - - def test_single_func(self): - self.assertEqual( - 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( - 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", - ) - - def test_escaped(self): - self.assertEqual( - 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. ", - ) - - def test_escaped2(self): - self.assertEqual( - 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. ", - )