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

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

View file

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

View file

@ -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":<object>, "key2":<object2>,...}. 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 `<object>.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":<object>, "key2":<object2>,...}.
The keys must either match `{key}` or `$You(key)/$you(key)` markers
in the `text` string. If `<object>` 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,

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

View file

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

View file

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

View file

@ -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 `<UNKNOWN>` 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: "<UNKNOWN>",
"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"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
_RE_TOKEN = re.compile(
r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
(?P<comma>(?<!\\)\,)| # , (argument sep)
(?P<end>(?<!\\)\))| # ) (possible end of func call)
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
(?P<escaped> # escaped tokens to re-insert sans backslash
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
(?P<rest> # 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)

View file

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