diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 50bfbb2786..5ea451b0af 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2123,7 +2123,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS): ) if "prototype" in self.switches: - modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj]) + modified = spawner.batch_update_objects_with_prototype( + prototype, objects=[obj], caller=self.caller) prototype_success = modified > 0 if not prototype_success: caller.msg("Prototype %s failed to apply." % prototype["key"]) @@ -3559,7 +3560,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): return try: n_updated = spawner.batch_update_objects_with_prototype( - prototype, objects=existing_objects + prototype, objects=existing_objects, caller=caller, ) except Exception: logger.log_trace() @@ -3811,7 +3812,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS): # proceed to spawning try: - for obj in spawner.spawn(prototype): + for obj in spawner.spawn(prototype, caller=self.caller): self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) if not prototype.get("location") and not noloc: # we don't hardcode the location in the prototype (unless the user diff --git a/evennia/contrib/tutorial_world/intro_menu.py b/evennia/contrib/tutorial_world/intro_menu.py index 199b3fc90d..1be58ea31d 100644 --- a/evennia/contrib/tutorial_world/intro_menu.py +++ b/evennia/contrib/tutorial_world/intro_menu.py @@ -695,27 +695,27 @@ If you want there is also some |wextra|n info for where to go beyond that. After playing through the tutorial-world quest, if you aim to make a game with Evennia you are wise to take a look at the |wEvennia documentation|n at - |yhttps://github.com/evennia/evennia/wiki|n + |yhttps://www.evennia.com/docs/latest|n - You can start by trying to build some stuff by following the |wBuilder quick-start|n: - |yhttps://github.com/evennia/evennia/wiki/Building-Quickstart|n + |yhttps://www.evennia.com/docs/latest/Building-Quickstart|n - The tutorial-world may or may not be your cup of tea, but it does show off several |wuseful tools|n of Evennia. You may want to check out how it works: - |yhttps://github.com/evennia/evennia/wiki/Tutorial-World-Introduction|n + |yhttps://www.evennia.com/docs/latest/Tutorial-World-Introduction|n - You can then continue looking through the |wTutorials|n and pick one that fits your level of understanding. - |yhttps://github.com/evennia/evennia/wiki/Tutorials|n + |yhttps://www.evennia.com/docs/latest/Tutorials|n - Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The Evennia community is very active and friendly and no question is too simple. You will often quickly get help. You can everything you need linked from - |yhttp://www.evennia.com|n + |yhttps://www.evennia.com|n # --------------------------------------------------------------------------------- diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 1d88c2e702..b1482a3dda 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -20,6 +20,7 @@ from evennia.objects.models import ObjectDB from evennia.scripts.scripthandler import ScriptHandler from evennia.commands import cmdset, command from evennia.commands.cmdsethandler import CmdSetHandler +from evennia.utils import funcparser from evennia.utils import create from evennia.utils import search from evennia.utils import logger @@ -47,6 +48,12 @@ _COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) # the sessid_max is based on the length of the db_sessid csv field (excluding commas) _SESSID_MAX = 16 if _MULTISESSION_MODE in (1, 3) else 1 +_MSG_CONTENTS_PARSER = funcparser.FuncParser( + {"you": funcparser.funcparser_callable_you, + "You": funcparser.funcparser_callable_You, + "conj": funcparser.funcparser_callable_conjugate + }) + class ObjectSessionHandler(object): """ @@ -717,64 +724,94 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): text (str or tuple): Message to send. If a tuple, this should be on the valid OOB outmessage form `(message, {kwargs})`, where kwargs are optional data passed to the `text` - outputfunc. + outputfunc. The message will be parsed for `{key}` formatting and + `$You/$you()/$You(key)` and `$conj(verb)` inline function callables. + The `key` is taken from the `mapping` kwarg {"key": object, ...}`. + The `mapping[key].get_display_name(looker=recipient)` will be called + for that key for every recipient of the string. exclude (list, optional): A list of objects not to send to. from_obj (Object, optional): An object designated as the "sender" of the message. See `DefaultObject.msg()` for more info. mapping (dict, optional): A mapping of formatting keys - `{"key":, "key2":,...}. The keys - must match `{key}` markers in the `text` if this is a string or - in the internal `message` if `text` is a tuple. These - formatting statements will be - replaced by the return of `.get_display_name(looker)` - for every looker in contents that receives the - message. This allows for every object to potentially - get its own customized string. - Keyword Args: - Keyword arguments will be passed on to `obj.msg()` for all - messaged objects. + `{"key":, "key2":,...}. + The keys must either match `{key}` or `$You(key)/$you(key)` markers + in the `text` string. If `` doesn't have a `get_display_name` + method, it will be returned as a string. If not set, a key `you` will + be auto-added to point to `from_obj` if given, otherwise to `self`. + **kwargs: Keyword arguments will be passed on to `obj.msg()` for all + messaged objects. Notes: - The `mapping` argument is required if `message` contains - {}-style format syntax. The keys of `mapping` should match - named format tokens, and its values will have their - `get_display_name()` function called for each object in - the room before substitution. If an item in the mapping does - not have `get_display_name()`, its string value will be used. + For 'actor-stance' reporting (You say/Name says), use the + `$You()/$you()/$You(key)` and `$conj(verb)` (verb-conjugation) + inline callables. This will use the respective `get_display_name()` + for all onlookers except for `from_obj or self`, which will become + 'You/you'. If you use `$You/you(key)`, the key must be in `mapping`. - Example: - Say Char is a Character object and Npc is an NPC object: + For 'director-stance' reporting (Name says/Name says), use {key} + syntax directly. For both `{key}` and `You/you(key)`, + `mapping[key].get_display_name(looker=recipient)` may be called + depending on who the recipient is. - char.location.msg_contents( - "{attacker} kicks {defender}", - mapping=dict(attacker=char, defender=npc), exclude=(char, npc)) + Examples: - This will result in everyone in the room seeing 'Char kicks NPC' - where everyone may potentially see different results for Char and Npc - depending on the results of `char.get_display_name(looker)` and - `npc.get_display_name(looker)` for each particular onlooker + Let's assume + - `player1.key -> "Player1"`, + `player1.get_display_name(looker=player2) -> "The First girl"` + - `player2.key -> "Player2"`, + `player2.get_display_name(looker=player1) -> "The Second girl"` + + Actor-stance: + :: + + char.location.msg_contents( + "$You() $conj(attack) $you(defender).", + mapping={"defender": player2}) + + - player1 will see `You attack The Second girl.` + - player2 will see 'The First girl attacks you.' + + Director-stance: + :: + + char.location.msg_contents( + "{attacker} attacks {defender}.", + mapping={"attacker:player1, "defender":player2}) + + - player1 will see: 'Player1 attacks The Second girl.' + - player2 will see: 'The First girl attacks Player2' """ # we also accept an outcommand on the form (message, {kwargs}) is_outcmd = text and is_iter(text) inmessage = text[0] if is_outcmd else text outkwargs = text[1] if is_outcmd and len(text) > 1 else {} + mapping = mapping or {} + you = from_obj or self + + if 'you' not in mapping: + mapping[you] = you contents = self.contents if exclude: exclude = make_iter(exclude) contents = [obj for obj in contents if obj not in exclude] - for obj in contents: - if mapping: - substitutions = { - t: sub.get_display_name(obj) if hasattr(sub, "get_display_name") else str(sub) - for t, sub in mapping.items() - } - outmessage = inmessage.format(**substitutions) - else: - outmessage = inmessage - obj.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs) + + for receiver in contents: + + # actor-stance replacements + inmessage = _MSG_CONTENTS_PARSER.parse( + inmessage, raise_errors=True, return_string=True, + you=you, receiver=receiver, mapping=mapping) + + # director-stance replacements + outmessage = inmessage.format( + **{key: obj.get_display_name(looker=receiver) + if hasattr(obj, "get_display_name") else str(obj) + for key, obj in mapping.items()}) + + receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs) def move_to( self, diff --git a/evennia/prototypes/menus.py b/evennia/prototypes/menus.py index 174f96e7eb..f9c5ba62b1 100644 --- a/evennia/prototypes/menus.py +++ b/evennia/prototypes/menus.py @@ -2115,7 +2115,8 @@ def _apply_diff(caller, **kwargs): objects = kwargs["objects"] back_node = kwargs["back_node"] diff = kwargs.get("diff", None) - num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects) + num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects, + caller=caller) caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) return back_node @@ -2483,7 +2484,7 @@ def _spawn(caller, **kwargs): if not prototype.get("location"): prototype["location"] = caller - obj = spawner.spawn(prototype) + obj = spawner.spawn(prototype, caller=caller) if obj: obj = obj[0] text = "|gNew instance|n {key} ({dbref}) |gspawned at location |n{loc}|n|g.|n".format( diff --git a/evennia/prototypes/protfuncs.py b/evennia/prototypes/protfuncs.py index e22fd5a46a..c501d888ad 100644 --- a/evennia/prototypes/protfuncs.py +++ b/evennia/prototypes/protfuncs.py @@ -1,33 +1,28 @@ """ -Protfuncs are function-strings embedded in a prototype and allows for a builder to create a -prototype with custom logics without having access to Python. The Protfunc is parsed using the -inlinefunc parser but is fired at the moment the spawning happens, using the creating object's -session as input. +Protfuncs are FuncParser-callables that can be embedded in a prototype to +provide custom logic without having access to Python. The protfunc is parsed at +the time of spawning, using the creating object's session as input. If the +protfunc returns a non-string, this is what will be added to the prototype. In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.: { ... - "key": "$funcname(arg1, arg2, ...)" + "key": "$funcname(args, kwargs)" ... } -and multiple functions can be nested (no keyword args are supported). The result will be used as the -value for that prototype key for that individual spawn. - -Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They -are specified as functions +Available protfuncs are either all callables in one of the modules of `settings.PROT_FUNC_MODULES` +or all callables added to a dict FUNCPARSER_CALLABLES in such a module. def funcname (*args, **kwargs) -where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia: +At spawn-time the spawner passes the following extra kwargs into each callable (in addition to +what is added in the call itself): - session (Session): The Session of the entity spawning using this prototype. - prototype (dict): The dict this protfunc is a part of. - current_key (str): The active key this value belongs to in the prototype. - - testing (bool): This is set if this function is called as part of the prototype validation; if - set, the protfunc should take care not to perform any persistent actions, such as operate on - objects or add things to the database. Any traceback raised by this function will be handled at the time of spawning and abort the spawn before any object is created/updated. It must otherwise return the value to store for the specified @@ -35,312 +30,26 @@ prototype key (this value must be possible to serialize in an Attribute). """ -from ast import literal_eval -from random import randint as base_randint, random as base_random, choice as base_choice -import re - -from evennia.utils import search -from evennia.utils.utils import justify as base_justify, is_iter, to_str - -_PROTLIB = None - -_RE_DBREF = re.compile(r"\#[0-9]+") +from evennia.utils import funcparser -# default protfuncs - - -def random(*args, **kwargs): +def protfunc_callable_protkey(*args, **kwargs): """ - Usage: $random() - Returns a random value in the interval [0, 1) - - """ - return base_random() - - -def randint(*args, **kwargs): - """ - Usage: $randint(start, end) - Returns random integer in interval [start, end] - - """ - if len(args) != 2: - raise TypeError("$randint needs two arguments - start and end.") - start, end = int(args[0]), int(args[1]) - return base_randint(start, end) - - -def left_justify(*args, **kwargs): - """ - Usage: $left_justify() - Returns left-justified. - - """ - if args: - return base_justify(args[0], align="l") - return "" - - -def right_justify(*args, **kwargs): - """ - Usage: $right_justify() - Returns right-justified across screen width. - - """ - if args: - return base_justify(args[0], align="r") - return "" - - -def center_justify(*args, **kwargs): - - """ - Usage: $center_justify() - Returns centered in screen width. - - """ - if args: - return base_justify(args[0], align="c") - return "" - - -def choice(*args, **kwargs): - """ - Usage: $choice(val, val, val, ...) - Returns one of the values randomly - """ - if args: - return base_choice(args) - return "" - - -def full_justify(*args, **kwargs): - - """ - Usage: $full_justify() - Returns filling up screen width by adding extra space. - - """ - if args: - return base_justify(args[0], align="f") - return "" - - -def protkey(*args, **kwargs): - """ - Usage: $protkey() + Usage: $protkey(keyname) Returns the value of another key in this prototoype. Will raise an error if the key is not found in this prototype. """ - if args: - prototype = kwargs["prototype"] - return prototype[args[0].strip()] + if not args: + return "" + + prototype = kwargs.get("prototype", {}) + return prototype[args[0].strip()] -def add(*args, **kwargs): - """ - Usage: $add(val1, val2) - Returns the result of val1 + val2. Values must be - valid simple Python structures possible to add, - such as numbers, lists etc. - - """ - if len(args) > 1: - val1, val2 = args[0], args[1] - # try to convert to python structures, otherwise, keep as strings - try: - val1 = literal_eval(val1.strip()) - except Exception: - pass - try: - val2 = literal_eval(val2.strip()) - except Exception: - pass - return val1 + val2 - raise ValueError("$add requires two arguments.") - - -def sub(*args, **kwargs): - """ - Usage: $del(val1, val2) - Returns the value of val1 - val2. Values must be - valid simple Python structures possible to - subtract. - - """ - if len(args) > 1: - val1, val2 = args[0], args[1] - # try to convert to python structures, otherwise, keep as strings - try: - val1 = literal_eval(val1.strip()) - except Exception: - pass - try: - val2 = literal_eval(val2.strip()) - except Exception: - pass - return val1 - val2 - raise ValueError("$sub requires two arguments.") - - -def mult(*args, **kwargs): - """ - Usage: $mul(val1, val2) - Returns the value of val1 * val2. The values must be - valid simple Python structures possible to - multiply, like strings and/or numbers. - - """ - if len(args) > 1: - val1, val2 = args[0], args[1] - # try to convert to python structures, otherwise, keep as strings - try: - val1 = literal_eval(val1.strip()) - except Exception: - pass - try: - val2 = literal_eval(val2.strip()) - except Exception: - pass - return val1 * val2 - raise ValueError("$mul requires two arguments.") - - -def div(*args, **kwargs): - """ - Usage: $div(val1, val2) - Returns the value of val1 / val2. Values must be numbers and - the result is always a float. - - """ - if len(args) > 1: - val1, val2 = args[0], args[1] - # try to convert to python structures, otherwise, keep as strings - try: - val1 = literal_eval(val1.strip()) - except Exception: - pass - try: - val2 = literal_eval(val2.strip()) - except Exception: - pass - return val1 / float(val2) - raise ValueError("$mult requires two arguments.") - - -def toint(*args, **kwargs): - """ - Usage: $toint() - Returns as an integer. - """ - if args: - val = args[0] - try: - return int(literal_eval(val.strip())) - except ValueError: - return val - raise ValueError("$toint requires one argument.") - - -def eval(*args, **kwargs): - """ - Usage $eval() - Returns evaluation of a simple Python expression. The string may *only* consist of the following - Python literal structures: strings, numbers, tuples, lists, dicts, booleans, - and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..) - - those will then be evaluated *after* $eval. - - """ - global _PROTLIB - if not _PROTLIB: - from evennia.prototypes import prototypes as _PROTLIB - - string = ",".join(args) - struct = literal_eval(string) - - if isinstance(struct, str): - # we must shield the string, otherwise it will be merged as a string and future - # literal_evas will pick up e.g. '2' as something that should be converted to a number - struct = '"{}"'.format(struct) - - # convert any #dbrefs to objects (also in nested structures) - struct = _PROTLIB.value_to_obj_or_any(struct) - - return struct - - -def _obj_search(*args, **kwargs): - "Helper function to search for an object" - - query = "".join(args) - session = kwargs.get("session", None) - return_list = kwargs.pop("return_list", False) - account = None - - if session: - account = session.account - - targets = search.search_object(query) - - if return_list: - retlist = [] - if account: - for target in targets: - if target.access(account, target, "control"): - retlist.append(target) - else: - retlist = targets - return retlist - else: - # single-match - if not targets: - raise ValueError("$obj: Query '{}' gave no matches.".format(query)) - if len(targets) > 1: - raise ValueError( - "$obj: Query '{query}' gave {nmatches} matches. Limit your " - "query or use $objlist instead.".format(query=query, nmatches=len(targets)) - ) - target = targets[0] - if account: - if not target.access(account, target, "control"): - raise ValueError( - "$obj: Obj {target}(#{dbref} cannot be added - " - "Account {account} does not have 'control' access.".format( - target=target.key, dbref=target.id, account=account - ) - ) - return target - - -def obj(*args, **kwargs): - """ - Usage $obj() - Returns one Object searched globally by key, alias or #dbref. Error if more than one. - - """ - obj = _obj_search(return_list=False, *args, **kwargs) - if obj: - return "#{}".format(obj.id) - return "".join(args) - - -def objlist(*args, **kwargs): - """ - Usage $objlist() - Returns list with one or more Objects searched globally by key, alias or #dbref. - - """ - return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)] - - -def dbref(*args, **kwargs): - """ - Usage $dbref(<#dbref>) - Validate that a #dbref input is valid. - """ - if not args or len(args) < 1 or _RE_DBREF.match(args[0]) is None: - raise ValueError("$dbref requires a valid #dbref argument.") - - return obj(args[0]) +# this is picked up by FuncParser +FUNCPARSER_CALLABLES = { + "protkey": protfunc_callable_protkey, + **funcparser.FUNCPARSER_CALLABLES, + **funcparser.SEARCHING_CALLABLES, +} diff --git a/evennia/prototypes/prototypes.py b/evennia/prototypes/prototypes.py index eba1fe7f06..042670ea05 100644 --- a/evennia/prototypes/prototypes.py +++ b/evennia/prototypes/prototypes.py @@ -31,7 +31,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 import dbserialize from evennia.utils.evtable import EvTable @@ -721,7 +721,7 @@ for mod in settings.PROT_FUNC_MODULES: raise -def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs): +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. @@ -741,6 +741,8 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F session (Session): Passed to protfunc. Session of the entity spawning the prototype. protototype (dict): Passed to protfunc. The dict this protfunc is a part of. current_key(str): Passed to protfunc. The key in the prototype that will hold this value. + caller (Object or Account): This is necessary for certain protfuncs that perform object + searches and have to check permissions. any (any): Passed on to the protfunc. Returns: @@ -759,11 +761,8 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F available_functions = PROT_FUNCS if available_functions is None else available_functions - result = FuncParser(available_functions).parse(value, raise_errors=True, **kwargs) - - # result = inlinefuncs.parse_inlinefunc( - # value, available_funcs=available_functions, stacktrace=stacktrace, testing=testing, **kwargs - # ) + result = FuncParser(available_functions).parse( + value, raise_errors=True, caller=caller, **kwargs) err = None try: diff --git a/evennia/prototypes/spawner.py b/evennia/prototypes/spawner.py index 0a5ec6012f..10bdba18d4 100644 --- a/evennia/prototypes/spawner.py +++ b/evennia/prototypes/spawner.py @@ -607,7 +607,8 @@ def format_diff(diff, minimal=True): return "\n ".join(line for line in texts if line) -def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False): +def batch_update_objects_with_prototype(prototype, diff=None, objects=None, + exact=False, caller=None): """ Update existing objects with the latest version of the prototype. @@ -624,6 +625,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac if it's not set in the prototype. With `exact=True`, all un-specified properties of the objects will be removed if they exist. This will lead to a more accurate 1:1 correlation between the object and the prototype but is usually impractical. + caller (Object or Account, optional): This may be used by protfuncs to do permission checks. Returns: changed (int): The number of objects that had changes applied to them. @@ -675,33 +677,33 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac do_save = True if key == "key": - obj.db_key = init_spawn_value(val, str) + obj.db_key = init_spawn_value(val, str, caller=caller) elif key == "typeclass": - obj.db_typeclass_path = init_spawn_value(val, str) + obj.db_typeclass_path = init_spawn_value(val, str, caller=caller) elif key == "location": - obj.db_location = init_spawn_value(val, value_to_obj) + obj.db_location = init_spawn_value(val, value_to_obj, caller=caller) elif key == "home": - obj.db_home = init_spawn_value(val, value_to_obj) + obj.db_home = init_spawn_value(val, value_to_obj, caller=caller) elif key == "destination": - obj.db_destination = init_spawn_value(val, value_to_obj) + obj.db_destination = init_spawn_value(val, value_to_obj, caller=caller) elif key == "locks": if directive == "REPLACE": obj.locks.clear() - obj.locks.add(init_spawn_value(val, str)) + obj.locks.add(init_spawn_value(val, str, caller=caller)) elif key == "permissions": if directive == "REPLACE": obj.permissions.clear() - obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val)) + obj.permissions.batch_add(*(init_spawn_value(perm, str, caller=caller) for perm in val)) elif key == "aliases": if directive == "REPLACE": obj.aliases.clear() - obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val)) + obj.aliases.batch_add(*(init_spawn_value(alias, str, caller=caller) for alias in val)) elif key == "tags": if directive == "REPLACE": obj.tags.clear() obj.tags.batch_add( *( - (init_spawn_value(ttag, str), tcategory, tdata) + (init_spawn_value(ttag, str, caller=caller), tcategory, tdata) for ttag, tcategory, tdata in val ) ) @@ -711,8 +713,8 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac obj.attributes.batch_add( *( ( - init_spawn_value(akey, str), - init_spawn_value(aval, value_to_obj), + init_spawn_value(akey, str, caller=caller), + init_spawn_value(aval, value_to_obj, caller=caller), acategory, alocks, ) @@ -723,7 +725,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exac # we don't auto-rerun exec statements, it would be huge security risk! pass else: - obj.attributes.add(key, init_spawn_value(val, value_to_obj)) + obj.attributes.add(key, init_spawn_value(val, value_to_obj, caller=caller)) elif directive == "REMOVE": do_save = True if key == "key": @@ -836,7 +838,7 @@ def batch_create_object(*objparams): # Spawner mechanism -def spawn(*prototypes, **kwargs): +def spawn(*prototypes, caller=None, **kwargs): """ Spawn a number of prototyped objects. @@ -845,6 +847,7 @@ def spawn(*prototypes, **kwargs): prototype_key (will be used to find the prototype) or a full prototype dictionary. These will be batched-spawned as one object each. Keyword Args: + caller (Object or Account, optional): This may be used by protfuncs to do access checks. prototype_modules (str or list): A python-path to a prototype module, or a list of such paths. These will be used to build the global protparents dictionary accessible by the input @@ -910,39 +913,39 @@ def spawn(*prototypes, **kwargs): "key", "Spawned-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:6]), ) - create_kwargs["db_key"] = init_spawn_value(val, str) + create_kwargs["db_key"] = init_spawn_value(val, str, caller=caller) val = prot.pop("location", None) - create_kwargs["db_location"] = init_spawn_value(val, value_to_obj) + create_kwargs["db_location"] = init_spawn_value(val, value_to_obj, caller=caller) val = prot.pop("home", None) if val: - create_kwargs["db_home"] = init_spawn_value(val, value_to_obj) + create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, caller=caller) else: try: - create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj) + create_kwargs["db_home"] = init_spawn_value(settings.DEFAULT_HOME, value_to_obj, caller=caller) except ObjectDB.DoesNotExist: # settings.DEFAULT_HOME not existing is common for unittests pass val = prot.pop("destination", None) - create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj) + create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj, caller=caller) val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) - create_kwargs["db_typeclass_path"] = init_spawn_value(val, str) + create_kwargs["db_typeclass_path"] = init_spawn_value(val, str, caller=caller) # extract calls to handlers val = prot.pop("permissions", []) - permission_string = init_spawn_value(val, make_iter) + permission_string = init_spawn_value(val, make_iter, caller=caller) val = prot.pop("locks", "") - lock_string = init_spawn_value(val, str) + lock_string = init_spawn_value(val, str, caller=caller) val = prot.pop("aliases", []) - alias_string = init_spawn_value(val, make_iter) + alias_string = init_spawn_value(val, make_iter, caller=caller) val = prot.pop("tags", []) tags = [] for (tag, category, *data) in val: - tags.append((init_spawn_value(tag, str), category, data[0] if data else None)) + tags.append((init_spawn_value(tag, str, caller=caller), category, data[0] if data else None)) prototype_key = prototype.get("prototype_key", None) if prototype_key: @@ -950,11 +953,11 @@ def spawn(*prototypes, **kwargs): tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY)) val = prot.pop("exec", "") - execs = init_spawn_value(val, make_iter) + execs = init_spawn_value(val, make_iter, caller=caller) # extract ndb assignments nattributes = dict( - (key.split("_", 1)[1], init_spawn_value(val, value_to_obj)) + (key.split("_", 1)[1], init_spawn_value(val, value_to_obj, caller=caller)) for key, val in prot.items() if key.startswith("ndb_") ) @@ -963,7 +966,7 @@ def spawn(*prototypes, **kwargs): val = make_iter(prot.pop("attrs", [])) attributes = [] for (attrname, value, *rest) in val: - attributes.append((attrname, init_spawn_value(value), + attributes.append((attrname, init_spawn_value(value, caller=caller), rest[0] if rest else None, rest[1] if len(rest) > 1 else None)) simple_attributes = [] @@ -975,7 +978,7 @@ def spawn(*prototypes, **kwargs): continue else: simple_attributes.append( - (key, init_spawn_value(value, value_to_obj_or_any), None, None) + (key, init_spawn_value(value, value_to_obj_or_any, caller=caller), None, None) ) attributes = attributes + simple_attributes diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 0d40566b7a..5fe08f02c1 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -611,7 +611,7 @@ INLINEFUNC_STACK_MAXSIZE = 20 # Only functions defined globally (and not starting with '_') in # these modules will be considered valid inlinefuncs. The list # is loaded from left-to-right, same-named functions will overload -INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs", "server.conf.inlinefuncs"] +INLINEFUNC_MODULES = ["evennia.utils.funcparser", "server.conf.inlinefuncs"] # Module holding handlers for ProtFuncs. These allow for embedding # functional code in prototypes and has the same syntax as inlinefuncs. PROTOTYPEFUNC_MODULES = ["evennia.prototypes.protfuncs", "server.conf.prototypefuncs"] diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 7ead30cab6..e19bdf7f75 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -820,15 +820,16 @@ def funcparser_callable_pad(*args, **kwargs): if not args: return '' text, *rest = args - nargs = len(args) + nrest = len(rest) try: - width = int(kwargs.get("width", rest[0] if nargs > 0 else _CLIENT_DEFAULT_WIDTH)) + width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH)) except TypeError: width = _CLIENT_DEFAULT_WIDTH - align = kwargs.get("align", rest[1] if nargs > 1 else 'c') - fillchar = kwargs.get("fillchar", rest[2] if nargs > 2 else ' ') - if fillchar not in ('c', 'l', 'r'): - fillchar = 'c' + + align = kwargs.get("align", rest[1] if nrest > 1 else 'c') + fillchar = kwargs.get("fillchar", rest[2] if nrest > 2 else ' ') + if align not in ('c', 'l', 'r'): + align = 'c' return pad(str(text), width=width, align=align, fillchar=fillchar) @@ -867,12 +868,12 @@ def funcparser_callable_crop(*args, **kwargs): if not args: return '' text, *rest = args - nargs = len(args) + nrest = len(rest) try: - width = int(kwargs.get("width", rest[0] if nargs > 0 else _CLIENT_DEFAULT_WIDTH)) + width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH)) except TypeError: width = _CLIENT_DEFAULT_WIDTH - suffix = kwargs.get('suffix', rest[1] if nargs > 1 else "[...]") + suffix = kwargs.get('suffix', rest[1] if nrest > 1 else "[...]") return crop(str(text), width=width, suffix=str(suffix)) @@ -896,14 +897,15 @@ def funcparser_callable_justify(*args, **kwargs): """ if not args: return '' - text = args[0] + text, *rest = args + lrest = len(rest) try: - width = int(kwargs.get("width", _CLIENT_DEFAULT_WIDTH)) + width = int(kwargs.get("width", rest[0] if lrest > 0 else _CLIENT_DEFAULT_WIDTH)) except TypeError: width = _CLIENT_DEFAULT_WIDTH - align = str(kwargs.get("align", 'f')) + align = str(kwargs.get("align", rest[1] if lrest > 1 else 'f')) try: - indent = int(kwargs.get("indent", 0)) + indent = int(kwargs.get("indent", rest[2] if lrest > 2 else 0)) except TypeError: indent = 0 return justify(str(text), width=width, align=align, indent=indent) @@ -912,17 +914,17 @@ def funcparser_callable_justify(*args, **kwargs): # legacy for backwards compatibility def funcparser_callable_left_justify(*args, **kwargs): "Usage: $ljust(text)" - return funcparser_callable_justify(*args, justify='l', **kwargs) + return funcparser_callable_justify(*args, align='l', **kwargs) def funcparser_callable_right_justify(*args, **kwargs): "Usage: $rjust(text)" - return funcparser_callable_justify(*args, justify='r', **kwargs) + return funcparser_callable_justify(*args, align='r', **kwargs) def funcparser_callable_center_justify(*args, **kwargs): "Usage: $cjust(text)" - return funcparser_callable_justify(*args, justify='c', **kwargs) + return funcparser_callable_justify(*args, align='c', **kwargs) def funcparser_callable_clr(*args, **kwargs): @@ -988,7 +990,8 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs): any: An entity match or None if no match or a list if `return_list` is set. Raise: - ParsingError: If zero/multimatch and `return_list` is False. + ParsingError: If zero/multimatch and `return_list` is False, or caller was not + passed into parser. Examples: - "$search(#233)" @@ -996,10 +999,12 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs): - "$search(meadow, return_list=True)" """ - return_list = bool(kwargs.get("return_list", "False")) + return_list = kwargs.get("return_list", "false").lower() == "true" - if not (args and caller): + if not args: return [] if return_list else None + if not caller: + raise ParsingError("$search requires a `caller` passed to the parser.") query = str(args[0]) @@ -1040,63 +1045,72 @@ def funcparser_callable_search_list(*args, caller=None, access="control", **kwar return_list=True, **kwargs) -def funcparser_callable_you(*args, you_obj=None, you_target=None, capitalize=False, **kwargs): +def funcparser_callable_you(*args, you=None, receiver=None, mapping=None, capitalize=False, **kwargs): """ - Usage: %you() + Usage: $you() or $you(key) Replaces with you for the caller of the string, with the display_name of the caller for others. Kwargs: - you_obj (Object): The object who represents 'you' in the string. - you_target (Object): The recipient of the string. + you (Object): The 'you' in the string. This is used unless another + you-key is passed to the callable in combination with `mapping`. + receiver (Object): The recipient of the string. + mapping (dict, optional): This is a mapping `{key:Object, ...}` and is + used to find which object `$you(key)` refers to. If not given, the + `you` kwarg is used. capitalize (bool): Passed by the You helper, to capitalize you. Returns: str: The parsed string. Raises: - ParsingError: If `you_obj` and `you_target` were not supplied. + ParsingError: If `you` and `receiver` were not supplied. Notes: - The kwargs must be supplied to the parse method. If not given, - the parsing will be aborted. Note that it will not capitalize + The kwargs should be passed the to parser directly. Examples: This can be used by the say or emote hooks to pass actor stance strings. This should usually be combined with the $inflect() callable. - - `With a grin, $you() $conj(jump).` + - `With a grin, $you() $conj(jump) at $you(tommy).` - The You-object will see "With a grin, you jump." - Others will see "With a grin, CharName jumps." + The You-object will see "With a grin, you jump at Tommy." + Tommy will see "With a grin, CharName jumps at you." + Others will see "With a grin, CharName jumps at Tommy." """ - if not (you_obj and you_target): - raise ParsingError("No you_obj/target supplied to $you callable") + if args and mapping: + # this would mean a $you(key) form + try: + you = mapping.get(args[0]) + except KeyError: + pass + + if not (you and receiver): + raise ParsingError("No you-object or receiver supplied to $you callable.") + capitalize = bool(capitalize) - if you_obj == you_target: + if you == receiver: return "You" if capitalize else "you" - return you_obj.get_display_name(looker=you_target) + return you.get_display_name(looker=receiver) if hasattr(you, "get_display_name") else str(you) -def funcparser_callable_You(*args, you_obj=None, you_target=None, capitalize=True, **kwargs): +def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs): """ Usage: $You() - capitalizes the 'you' output. """ return funcparser_callable_you( - *args, you_obj=you_obj, you_target=you_target, capitalize=capitalize, **kwargs) + *args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs) -def funcparser_callable_conjugate(*args, you_obj=None, you_target=None, **kwargs): +def funcparser_callable_conjugate(*args, you=None, receiver=None, **kwargs): """ - Conjugate a verb according to if it should be 2nd or third person. The - mlconjug3 package supports French, English, Italian, Portugese and Romanian - (see https://pypi.org/project/mlconjug3/). The function will pick the - language from settings.LANGUAGE_CODE, or English if an unsupported language - was found. + $conj(verb) + Conjugate a verb according to if it should be 2nd or third person. Kwargs: you_obj (Object): The object who represents 'you' in the string. you_target (Object): The recipient of the string. @@ -1105,34 +1119,39 @@ def funcparser_callable_conjugate(*args, you_obj=None, you_target=None, **kwargs str: The parsed string. Raises: - ParsingError: If `you_obj` and `you_target` were not supplied. + ParsingError: If `you` and `recipient` were not both supplied. Notes: - The kwargs must be supplied to the parse method. If not given, - the parsing will be aborted. Note that it will not capitalize + Note that it will not capitalized. + This assumes that the active party (You) is the one performing the verb. + This automatic conjugation will fail if the active part is another person + than 'you'. + The you/receiver should be passed to the parser directly. Exampels: This is often used in combination with the $you/You( callables. - `With a grin, $you() $conj(jump)` - The You-object will see "With a grin, you jump." + You will see "With a grin, you jump." Others will see "With a grin, CharName jumps." """ if not args: return '' - if not (you_obj and you_target): - raise ParsingError("No you_obj/target supplied to $conj callable") + if not (you and receiver): + raise ParsingError("No youj/receiver supplied to $conj callable") - you_str, them_str = verb_actor_stance_components(args[0]) - return you_str if you_obj == you_target else them_str + second_person_str, third_person_str = verb_actor_stance_components(args[0]) + return second_person_str if you == receiver else third_person_str # these are made available as callables by adding 'evennia.utils.funcparser' as # a callable-path when initializing the FuncParser. FUNCPARSER_CALLABLES = { + # 'standard' callables + # eval and arithmetic "eval": funcparser_callable_eval, "add": funcparser_callable_add, @@ -1160,14 +1179,18 @@ FUNCPARSER_CALLABLES = { "justify_center": funcparser_callable_center_justify, "space": funcparser_callable_space, "clr": funcparser_callable_clr, +} - # seaching +SEARCHING_CALLABLES = { + # requires `caller` and optionally `access` to be passed into parser "search": funcparser_callable_search, "obj": funcparser_callable_search, # aliases for backwards compat "objlist": funcparser_callable_search_list, "dbref": funcparser_callable_search, +} - # referencing +ACTOR_STANCE_CALLABLES = { + # requires `you`, `receiver` and `mapping` to be passed into parser "you": funcparser_callable_you, "You": funcparser_callable_You, "conj": funcparser_callable_conjugate, diff --git a/evennia/utils/inlinefuncs.py b/evennia/utils/inlinefuncs.py deleted file mode 100644 index 6d82407743..0000000000 --- a/evennia/utils/inlinefuncs.py +++ /dev/null @@ -1,529 +0,0 @@ -""" -Inline functions (nested form). - -This parser accepts nested inlinefunctions on the form - -```python -$funcname(arg, arg, ...) -``` - -embedded in any text where any arg can be another ``$funcname()`` call. -This functionality is turned off by default - to activate, -`settings.INLINEFUNC_ENABLED` must be set to `True`. - -Each token starts with `$funcname(` where there must be no space -between the `$funcname` and `"("`. The inlinefunc ends with a matched ending parentesis. -`")"`. - -Inside the inlinefunc definition, one can use `\` to escape. This is -mainly needed for escaping commas in flowing text (which would -otherwise be interpreted as an argument separator), or to escape `)` -when not intended to close the function block. Enclosing text in -matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will -also escape *everything* within without needing to escape individual -characters. - -The available inlinefuncs are defined as global-level functions in -modules defined by `settings.INLINEFUNC_MODULES`. They are identified -by their function name (and ignored if this name starts with `_`). They -should be on the following form: - -```python -def funcname (*args, **kwargs): - # ... -``` - -Here, the arguments given to `$funcname(arg1,arg2)` will appear as the -`*args` tuple. This will be populated by the arguments given to the -inlinefunc in-game - the only part that will be available from -in-game. `**kwargs` are not supported from in-game but are only used -internally by Evennia to make details about the caller available to -the function. The kwarg passed to all functions is `session`, the -Sessionobject for the object seeing the string. This may be `None` if -the string is sent to a non-puppetable object. The inlinefunc should -never raise an exception. - -There are two reserved function names: - -- "nomatch": This is called if the user uses a functionname that is - not registered. The nomatch function will get the name of the - not-found function as its first argument followed by the normal - arguments to the given function. If not defined the default effect is - to print `` to replace the unknown function. -- "stackfull": This is called when the maximum nested function stack is reached. - When this happens, the original parsed string is returned and the result of - the `stackfull` inlinefunc is appended to the end. By default this is an - error message. - -Syntax errors, notably failing to completely closing all inlinefunc -blocks, will lead to the entire string remaining unparsed. Inlineparsing should -never traceback. - ----- - -""" - -import re -import fnmatch -import random as base_random -from django.conf import settings - -from evennia.utils import utils, logger - -# The stack size is a security measure. Set to <=0 to disable. -_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE - - -# example/testing inline functions - - -def random(*args, **kwargs): - """ - Inlinefunc. Returns a random number between - 0 and 1, from 0 to a maximum value, or within a given range (inclusive). - - Args: - minval (str, optional): Minimum value. If not given, assumed 0. - maxval (str, optional): Maximum value. - - Keyword argumuents: - session (Session): Session getting the string. - - Notes: - If either of the min/maxvalue has a '.' in it, a floating-point random - value will be returned. Otherwise it will be an integer value in the - given range. - - Example: - `$random()` - `$random(5)` - `$random(5, 10)` - - """ - nargs = len(args) - if nargs == 1: - # only maxval given - minval, maxval = "0", args[0] - elif nargs > 1: - minval, maxval = args[:2] - else: - minval, maxval = ("0", "1") - - if "." in minval or "." in maxval: - # float mode - try: - minval, maxval = float(minval), float(maxval) - except ValueError: - minval, maxval = 0, 1 - return "{:.2f}".format(minval + maxval * base_random.random()) - else: - # int mode - try: - minval, maxval = int(minval), int(maxval) - except ValueError: - minval, maxval = 0, 1 - return str(base_random.randint(minval, maxval)) - - -def pad(*args, **kwargs): - """ - Inlinefunc. Pads text to given width. - - Args: - text (str, optional): Text to pad. - width (str, optional): Will be converted to integer. Width - of padding. - align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'. - fillchar (str, optional): Character used for padding. Defaults to a - space. - - Keyword Args: - session (Session): Session performing the pad. - - Example: - `$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(*args, **kwargs): - """ - Inlinefunc. Crops ingoing text to given widths. - - Args: - text (str, optional): Text to crop. - width (str, optional): Will be converted to an integer. Width of - crop in characters. - suffix (str, optional): End string to mark the fact that a part - of the string was cropped. Defaults to `[...]`. - Keyword Args: - session (Session): Session performing the crop. - - Example: - `$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) - - -def space(*args, **kwargs): - """ - Inlinefunc. Inserts an arbitrary number of spaces. Defaults to 4 spaces. - - Args: - spaces (int, optional): The number of spaces to insert. - - Keyword Args: - session (Session): Session performing the crop. - - Example: - `$space(20)` - - """ - width = 4 - if args: - width = abs(int(args[0])) if args[0].strip().isdigit() else 4 - return " " * width - - -def clr(*args, **kwargs): - """ - Inlinefunc. Colorizes nested text. - - Args: - startclr (str, optional): An ANSI color abbreviation without the - prefix `|`, such as `r` (red foreground) or `[r` (red background). - text (str, optional): Text - endclr (str, optional): The color to use at the end of the string. Defaults - to `|n` (reset-color). - Keyword Args: - session (Session): Session object triggering inlinefunc. - - Example: - `$clr(startclr, text, endclr)` - - """ - text = "" - nargs = len(args) - if nargs > 0: - color = args[0].strip() - if nargs > 1: - text = args[1] - text = "|" + color + text - if nargs > 2: - text += "|" + args[2].strip() - else: - text += "|n" - return text - - -def null(*args, **kwargs): - return args[0] if args else "" - - -def nomatch(name, *args, **kwargs): - """ - Default implementation of nomatch returns the function as-is as a string. - - """ - kwargs.pop("inlinefunc_stack_depth", None) - kwargs.pop("session") - - return "${name}({args}{kwargs})".format( - name=name, - args=",".join(args), - kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()), - ) - - -_INLINE_FUNCS = {} - -# we specify a default nomatch function to use if no matching func was -# found. This will be overloaded by any nomatch function defined in -# the imported modules. -_DEFAULT_FUNCS = { - "nomatch": lambda *args, **kwargs: "", - "stackfull": lambda *args, **kwargs: "\n (not parsed: ", -} - -_INLINE_FUNCS.update(_DEFAULT_FUNCS) - -# load custom inline func modules. -for module in utils.make_iter(settings.INLINEFUNC_MODULES): - try: - _INLINE_FUNCS.update(utils.callables_from_module(module)) - except ImportError as err: - if module == "server.conf.inlinefuncs": - # a temporary warning since the default module changed name - raise ImportError( - "Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " - "be renamed to mygame/server/conf/inlinefuncs.py (note " - "the S at the end)." % err - ) - else: - raise - - -# regex definitions - -_RE_STARTTOKEN = re.compile(r"(?.*?)(?.*?)(?(?(?(?(? # escaped tokens to re-insert sans backslash - \\\'|\\\"|\\\)|\\\$\w+\(|\\\()| - (?P # everything else to re-insert verbatim - \$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""", - re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL, -) - -# Cache for function lookups. -_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000) - - -class ParseStack(list): - """ - Custom stack that always concatenates strings together when the - strings are added next to one another. Tuples are stored - separately and None is used to mark that a string should be broken - up into a new chunk. Below is the resulting stack after separately - appending 3 strings, None, 2 strings, a tuple and finally 2 - strings: - - [string + string + string, - None - string + string, - tuple, - string + string] - - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # always start stack with the empty string - list.append(self, "") - # indicates if the top of the stack is a string or not - self._string_last = True - - def __eq__(self, other): - return ( - super().__eq__(other) - and hasattr(other, "_string_last") - and self._string_last == other._string_last - ) - - def __ne__(self, other): - return not self.__eq__(other) - - def append(self, item): - """ - The stack will merge strings, add other things as normal - """ - if isinstance(item, str): - if self._string_last: - self[-1] += item - else: - list.append(self, item) - self._string_last = True - else: - # everything else is added as normal - list.append(self, item) - 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) diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index 62f1a8f177..3fec8f59d5 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -10,7 +10,7 @@ from simpleeval import simple_eval from parameterized import parameterized from django.test import TestCase, override_settings -from evennia.utils import funcparser +from evennia.utils import funcparser, test_resources def _test_callable(*args, **kwargs): @@ -300,33 +300,27 @@ class TestDefaultCallables(TestCase): @parameterized.expand([ ("$You() $conj(smile) at him.", "You smile at him.", "Char1 smiles at him."), + ("$You() $conj(smile) at $You(char1).", "You smile at You.", "Char1 smiles at Char1."), + ("$You() $conj(smile) at $You(char2).", "You smile at Char2.", "Char1 smiles at You."), + ("$You(char2) $conj(smile) at $you(char1).", "Char2 smile at you.", "You smiles at Char1."), ]) def test_conjugate(self, string, expected_you, expected_them): """ Test callables with various input strings """ - ret = self.parser.parse(string, you_obj=self.obj1, you_target=self.obj1, + mapping = {"char1": self.obj1, "char2": self.obj2} + ret = self.parser.parse(string, you=self.obj1, receiver=self.obj1, mapping=mapping, raise_errors=True) self.assertEqual(expected_you, ret) - ret = self.parser.parse(string, you_obj=self.obj1, you_target=self.obj2, + ret = self.parser.parse(string, you=self.obj1, receiver=self.obj2, mapping=mapping, raise_errors=True) self.assertEqual(expected_them, ret) - -class TestOldDefaultCallables(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 $pad(Hello, width=20, align=c, fillchar=-) 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"), @@ -335,8 +329,13 @@ class TestOldDefaultCallables(TestCase): ("Some $mult(3, 2) things", "Some 6 things"), ("Some $div(6, 2) things", "Some 3.0 things"), ("Some $toint(6) things", "Some 6 things"), + ("Some $ljust(Hello, 30)", "Some Hello "), + ("Some $rjust(Hello, 30)", "Some Hello"), + ("Some $rjust(Hello, width=30)", "Some Hello"), + ("Some $cjust(Hello, 30)", "Some Hello "), + ("Some $eval('-'*20)Hello", "Some --------------------Hello"), ]) - def test_callable(self, string, expected): + def test_other_callables(self, string, expected): """ Test default callables. @@ -349,3 +348,60 @@ class TestOldDefaultCallables(TestCase): ret = self.parser.parse(string, raise_errors=True) ret = int(ret) self.assertTrue(1 <= ret <= 10) + + +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) + + def test_search_obj(self): + """ + Test searching for an object + + """ + string = "$search(Char)" + expected = self.char1 + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + self.assertEqual(expected, ret) + + def test_search_account(self): + """ + Test searching for an account + + """ + string = "$search(TestAccount, type=account)" + expected = self.account + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + self.assertEqual(expected, ret) + + def test_search_script(self): + """ + Test searching for a script + + """ + string = "$search(Script, type=script)" + expected = self.script + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + self.assertEqual(expected, ret) + + def test_search_obj_embedded(self): + """ + Test searching for an object - embedded in str + + """ + string = "This is $search(Char) the guy." + expected = "This is " + str(self.char1) + " the guy." + + ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) + self.assertEqual(expected, ret)