Homogenize funcparser calls

This commit is contained in:
Griatch 2021-03-25 23:15:47 +01:00
parent adb370b1d3
commit a3a57314a1
11 changed files with 293 additions and 993 deletions

View file

@ -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(

View file

@ -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(<text>)
Returns <text> left-justified.
"""
if args:
return base_justify(args[0], align="l")
return ""
def right_justify(*args, **kwargs):
"""
Usage: $right_justify(<text>)
Returns <text> right-justified across screen width.
"""
if args:
return base_justify(args[0], align="r")
return ""
def center_justify(*args, **kwargs):
"""
Usage: $center_justify(<text>)
Returns <text> 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(<text>)
Returns <text> filling up screen width by adding extra space.
"""
if args:
return base_justify(args[0], align="f")
return ""
def protkey(*args, **kwargs):
"""
Usage: $protkey(<key>)
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(<number>)
Returns <number> 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(<expression>)
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(<query>)
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(<query>)
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,
}

View file

@ -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:

View file

@ -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