Clean up docs and more funcparser fixes

This commit is contained in:
Griatch 2021-03-27 23:43:46 +01:00
parent c65c68e4c2
commit c9d9e9c6f8
21 changed files with 438 additions and 512 deletions

View file

@ -2384,7 +2384,7 @@ class CmdExamine(ObjManipCommand):
"""
global _FUNCPARSER
if not _FUNCPARSER:
_FUNCPARSER = funcparser.FuncParser(settings.INLINEFUNC_MODULES)
_FUNCPARSER = funcparser.FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES)
if attr is None:
return "No such attribute was found."
@ -3464,7 +3464,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
"Python structures are allowed. \nMake sure to use correct "
"Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n For more advanced uses, embed "
"inlinefuncs in the strings."
"funcparser callables ($funcs) in the strings."
)
else:
string = "Expected {}, got {}.".format(expect, type(prototype))

View file

@ -1,17 +1,17 @@
"""
Inlinefunc
Outgoing callables to apply with the FuncParser on outgoing messages.
Inline functions allow for direct conversion of text users mark in a
special way. Inlinefuncs are deactivated by default. To activate, add
The functions in this module will become available as $funcname(args, kwargs)
in all outgoing strings if you add
INLINEFUNC_ENABLED = True
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = True
to your settings file. The default inlinefuncs are found in
evennia.utils.inlinefunc.
to your settings file. The default inlinefuncs are found at the bottom of
`evennia.utils.funcparser`.
In text, usage is straightforward:
$funcname([arg1,[arg2,...]])
$funcname(arg1, arg2, ..., key=val, key2=val2, ...)
Example 1 (using the "pad" inlinefunc):
say This is $pad("a center-padded text", 50,c,-) of width 50.
@ -26,26 +26,14 @@ Example 2 (using nested "pad" and "time" inlinefuncs):
To add more inline functions, add them to this module, using
the following call signature:
def funcname(text, *args, **kwargs)
where `text` is always the part between {funcname(args) and
{/funcname and the *args are taken from the appropriate part of the
call. If no {/funcname is given, `text` will be the empty string.
It is important that the inline function properly clean the
incoming `args`, checking their type and replacing them with sane
defaults if needed. If impossible to resolve, the unmodified text
should be returned. The inlinefunc should never cause a traceback.
While the inline function should accept **kwargs, the keyword is
never accepted as a valid call - this is only intended to be used
internally by Evennia, notably to send the `session` keyword to
the function; this is the session of the object viewing the string
and can be used to customize it to each session.
def funcname(*args, **kwargs)
...
"""
# def capitalize(text, *args, **kwargs):
# "Silly capitalize example. Used as {capitalize() ... {/capitalize"
# def capitalize(*args, **kwargs):
# "Silly capitalize example. Used as $capitalize
# if not args:
# return ''
# session = kwargs.get("session")
# return text.capitalize()
# return args[0].capitalize()

View file

@ -45,7 +45,12 @@ def protfunc_callable_protkey(*args, **kwargs):
prototype = kwargs.get("prototype", {})
prot_value = prototype[args[0]]
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
try:
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
except funcparser.ParsingError:
return prot_value
# this is picked up by FuncParser

View file

@ -59,6 +59,21 @@ def check_errors(settings):
"Update your settings file (see evennia/settings_default.py "
"for more info)."
)
depstring = (
"settings.{} was renamed to {}. Update your settings file (the FuncParser "
"replaces and generalizes that which inlinefuncs used to do).")
if hasattr(settings, "INLINEFUNC_ENABLED"):
raise DeprecationWarning(depstring.format(
"settings.INLINEFUNC_ENABLED", "FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLE"))
if hasattr(settings, "INLINEFUNC_STACK_MAXSIZE"):
raise DeprecationWarning(depstring.format(
"settings.INLINEFUNC_STACK_MAXSIZE", "FUNCPARSER_MAX_NESTING"))
if hasattr(settings, "INLINEFUNC_MODULES"):
raise DeprecationWarning(depstring.format(
"settings.INLINEFUNC_MODULES", "FUNCPARSER_OUTGOING_MESSAGES_MODULES"))
if hasattr(settings, "PROTFUNC_MODULES"):
raise DeprecationWarning(depstring.format(
"settings.PROTFUNC_MODULES", "FUNCPARSER_PROTOTYPE_VALUE_MODULES"))
gametime_deprecation = (
"The settings TIME_SEC_PER_MIN, TIME_MIN_PER_HOUR,"

View file

@ -28,10 +28,9 @@ from evennia.utils.utils import (
from evennia.server.portal import amp
from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT
from evennia.server.signals import SIGNAL_ACCOUNT_POST_FIRST_LOGIN, SIGNAL_ACCOUNT_POST_LAST_LOGOUT
# from evennia.utils.inlinefuncs import parse_inlinefunc
from codecs import decode as codecs_decode
_INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED
_FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = settings.FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED
# delayed imports
_AccountDB = None
@ -154,7 +153,8 @@ class SessionHandler(dict):
def clean_senddata(self, session, kwargs):
"""
Clean up data for sending across the AMP wire. Also apply INLINEFUNCS.
Clean up data for sending across the AMP wire. Also apply the
FuncParser using callables from `settings.FUNCPARSER_OUTGOING_MESSAGES_MODULES`.
Args:
session (Session): The relevant session instance.
@ -170,14 +170,14 @@ class SessionHandler(dict):
Returns:
kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs,
where the keys, args and kwargs have all been converted to
send-safe entities (strings or numbers), and inlinefuncs have been
send-safe entities (strings or numbers), and funcparser parsing has been
applied.
"""
global _FUNCPARSER
if not _FUNCPARSER:
from evennia.utils.funcparser import FuncParser
_FUNCPARSER = FuncParser(settings.INLINEFUNC_MODULES, raise_errors=True)
_FUNCPARSER = FuncParser(settings.FUNCPARSER_OUTGOING_MESSAGES_MODULE, raise_errors=True)
options = kwargs.pop("options", None) or {}
raw = options.get("raw", False)
@ -210,8 +210,8 @@ class SessionHandler(dict):
elif isinstance(data, (str, bytes)):
data = _utf8(data)
if _INLINEFUNC_ENABLED and not raw and isinstance(self, ServerSessionHandler):
# only parse inlinefuncs on the outgoing path (sessionhandler->)
if _FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED and not raw and isinstance(self, ServerSessionHandler):
# only apply funcparser on the outgoing path (sessionhandler->)
# data = parse_inlinefunc(data, strip=strip_inlinefunc, session=session)
data = _FUNCPARSER.parse(data, strip=strip_inlinefunc, session=session)

View file

@ -598,23 +598,31 @@ TIME_GAME_EPOCH = None
TIME_IGNORE_DOWNTIMES = False
######################################################################
# Inlinefunc, PrototypeFuncs
# FuncParser
#
# Strings parsed with the FuncParser can contain 'callables' on the
# form $funcname(args,kwargs), which will lead to actual Python functions
# being executed.
######################################################################
# Evennia supports inline function preprocessing. This allows users
# to supply inline calls on the form $func(arg, arg, ...) to do
# session-aware text formatting and manipulation on the fly. If
# disabled, such inline functions will not be parsed.
INLINEFUNC_ENABLED = False
# This defined how deeply nested inlinefuncs can be. Set to <=0 to
# disable (not recommended, this is a safeguard against infinite loops).
INLINEFUNC_STACK_MAXSIZE = 20
# This changes the start-symbol for the funcparser callable. Note that
# this will make a lot of documentation invalid and there may also be
# other unexpected side effects, so change with caution.
FUNCPARSER_START_CHAR = '$'
# The symbol to use to escape Func
FUNCPARSER_ESCAPE_CHAR = '\\'
# This is the global max nesting-level for nesting functions in
# the funcparser. This protects against infinite loops.
FUNCPARSER_MAX_NESTING = 20
# Activate funcparser for all outgoing strings. The current Session
# will be passed into the parser (used to be called inlinefuncs)
FUNCPARSER_PARSE_OUTGOING_MESSAGES_ENABLED = False
# 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.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"]
FUNCPARSER_OUTGOING_MESSAGES_MODULES = ["evennia.utils.funcparser", "server.conf.inlinefuncs"]
# Prototype values are also parsed with FuncParser. These modules
# define which $func callables are available to use in prototypes.
FUNCPARSER_PROTOTYPE_PARSING_MODULES = ["evennia.prototypes.protfuncs", "server.conf.prototypefuncs"]
######################################################################
# Global Scripts

View file

@ -2,10 +2,13 @@
Generic function parser for functions embedded in a string, on the form
`$funcname(*args, **kwargs)`, for example:
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
```
"A string $foo() with $bar(a, b, c, $moo(), d=23) etc."
```
Each arg/kwarg can also be another nested function. These will be executed from
the deepest-nested first and used as arguments for the higher-level function.
Each arg/kwarg can also be another nested function. These will be executed
inside-out and their return will used as arguments for the enclosing function
(so the same as for regular Python function execution).
This is the base for all forms of embedded func-parsing, like inlinefuncs and
protfuncs. Each function available to use must be registered as a 'safe'
@ -23,7 +26,6 @@ def funcname(*args, **kwargs):
# it must always accept *args and **kwargs.
...
return something
```
Usage:
@ -38,6 +40,7 @@ result = parser.parse("String with $funcname() in it")
The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
---
"""
import re
@ -53,20 +56,21 @@ from evennia.utils.utils import (
from evennia.utils import search
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_MAX_NESTING = 20
# setup
_ESCAPE_CHAR = "\\"
_START_CHAR = "$"
_CLIENT_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
_MAX_NESTING = settings.FUNCPARSER_MAX_NESTING
_START_CHAR = settings.FUNCPARSER_START_CHAR
_ESCAPE_CHAR = settings.FUNCPARSER_ESCAPE_CHAR
@dataclasses.dataclass
class ParsedFunc:
class _ParsedFunc:
"""
Represents a function parsed from the string
"""
prefix: str = "$"
prefix: str = _START_CHAR
funcname: str = ""
args: list = dataclasses.field(default_factory=list)
kwargs: dict = dataclasses.field(default_factory=dict)
@ -98,7 +102,8 @@ class ParsingError(RuntimeError):
class FuncParser:
"""
Sets up a parser for strings containing $funcname(*args, **kwargs) substrings.
Sets up a parser for strings containing `$funcname(*args, **kwargs)`
substrings.
"""
@ -113,26 +118,23 @@ class FuncParser:
Args:
callables (str, module, list or dict): Where to find
'safe' functions to make available in the parser. These modules
can have a dict `FUNCPARSER_CALLABLES = {"funcname": callable, ...}`.
If no such dict exists, all callables in provided modules (whose names
don't start with an underscore) will be loaded as callables. Each
callable will will be available to call as `$funcname(*args, **kwags)`
during parsing. If `callables` is a `str`, this should be the path
to such a module. A `list` can either be a list of paths or module
objects. If a `dict`, this should be a direct mapping
`{"funcname": callable, ...}` to use.
'safe' functions to make available in the parser. If a `dict`,
it should be a direct mapping `{"funcname": callable, ...}`. If
one or mode modules or module-paths, the module(s) are first checked
for a dict `FUNCPARSER_CALLABLES = {"funcname", callable, ...}`. If
no such variable exists, all callables in the module (whose name does
not start with an underscore) will be made available to the parser.
start_char (str, optional): A character used to identify the beginning
of a parseable function. Default is `$`.
escape_char (str, optional): Prepend characters with this to have
them not count as a function. Default is `\\`.
them not count as a function. Default is the backtick, `\\\\`.
max_nesting (int, optional): How many levels of nested function calls
are allowed, to avoid exploitation. Default is 20.
**default_kwargs: These kwargs will be passed into all callables. These
kwargs can be overridden both by kwargs passed direcetly to `.parse` _and_
kwargs can be overridden both by kwargs passed direcetly to `.parse` *and*
by kwargs given directly in the string `$funcname` call. They are
suitable for global defaults that is intended to be changed by the
user. To _guarantee_ a call always gets a particular kwarg, pass it
user. To guarantee a call always gets a particular kwarg, pass it
into `.parse` as `**reserved_kwargs` instead.
"""
@ -194,7 +196,7 @@ class FuncParser:
Execute a parsed function
Args:
parsedfunc (ParsedFunc): This dataclass holds the parsed details
parsedfunc (_ParsedFunc): This dataclass holds the parsed details
of the function.
raise_errors (bool, optional): Raise errors. Otherwise return the
string with the function unparsed.
@ -254,9 +256,9 @@ class FuncParser:
def parse(self, string, raise_errors=False, escape=False,
strip=False, return_str=True, **reserved_kwargs):
"""
Use parser to parse a string that may or may not have `$funcname(*args, **kwargs)`
- style tokens in it. Only the callables used to initiate the parser
will be eligible for parsing, others will remain un-parsed.
Use parser to parse a string that may or may not have
`$funcname(*args, **kwargs)` - style tokens in it. Only the callables
used to initiate the parser will be eligible for parsing.
Args:
string (str): The string to parse.
@ -357,7 +359,7 @@ class FuncParser:
callstack.append(curr_func)
# start a new func
curr_func = ParsedFunc(prefix=char, fullstr=char)
curr_func = _ParsedFunc(prefix=char, fullstr=char)
continue
if not curr_func:
@ -592,16 +594,16 @@ def funcparser_callable_eval(*args, **kwargs):
incoming string into a python object. If it fails, the return will be same
as the input.
Args
Args:
string (str): The string to parse. Only simple literals or operators are allowed.
Returns:
any: The string parsed into its Python form, or the same as input.
Example:
`$py(1)`
`$py([1,2,3,4])`
`$py(3 + 4)`
Examples:
- `$py(1) -> 1`
- `$py([1,2,3,4] -> [1, 2, 3]`
- `$py(3 + 4) -> 7`
"""
args, kwargs = safe_convert_to_types(("py", {}) , *args, **kwargs)
@ -652,22 +654,22 @@ def _apply_operation_two_elements(*args, operator="+", **kwargs):
def funcparser_callable_add(*args, **kwargs):
"""Usage: $add(val1, val2) -> val1 + val2"""
"""Usage: `$add(val1, val2) -> val1 + val2`"""
return _apply_operation_two_elements(*args, operator='+', **kwargs)
def funcparser_callable_sub(*args, **kwargs):
"""Usage: $sub(val1, val2) -> val1 - val2"""
"""Usage: ``$sub(val1, val2) -> val1 - val2`"""
return _apply_operation_two_elements(*args, operator='-', **kwargs)
def funcparser_callable_mult(*args, **kwargs):
"""Usage: $mult(val1, val2) -> val1 * val2"""
"""Usage: `$mult(val1, val2) -> val1 * val2`"""
return _apply_operation_two_elements(*args, operator='*', **kwargs)
def funcparser_callable_div(*args, **kwargs):
"""Usage: $mult(val1, val2) -> val1 / val2"""
"""Usage: `$mult(val1, val2) -> val1 / val2`"""
return _apply_operation_two_elements(*args, operator='/', **kwargs)
@ -686,8 +688,8 @@ def funcparser_callable_round(*args, **kwargs):
any: The rounded value or inp if inp was not a number.
Examples:
- `$round(3.5434343, 3)` - gives 3.543
- `$round($random(), 2)` - rounds random result, e.g 0.22
- `$round(3.5434343, 3) -> 3.543`
- `$round($random(), 2)` - rounds random result, e.g `0.22`
"""
if not args:
@ -738,7 +740,7 @@ def funcparser_callable_random(*args, **kwargs):
try:
if isinstance(minval, float) or isinstance(maxval, float):
return minval + maxval * random.random()
return minval + ((maxval - minval) * random.random())
else:
return random.randint(minval, maxval)
except Exception:
@ -794,8 +796,8 @@ def funcparser_callable_pad(*args, **kwargs):
fillchar (str, optional): Character used for padding. Defaults to a space.
Example:
- `$pad(text, 12, l, ' ')`
- `$pad(text, width=12, align=c, fillchar=-)`
- `$pad(text, 12, r, ' ') -> " text"`
- `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`
"""
if not args:
@ -829,8 +831,8 @@ def funcparser_callable_crop(*args, **kwargs):
of the string was cropped. Defaults to `[...]`.
Example:
`$crop(text, 78, [...])`
`$crop(text, width=78, suffix='[...]')`
- `$crop(A long text, 10, [...]) -> "A lon[...]"`
- `$crop(text, width=11, suffix='[...]) -> "A long[...]"`
"""
if not args:
@ -875,8 +877,8 @@ def funcparser_callable_justify(*args, **kwargs):
str: The justified text.
Examples:
- $just(text, width=40)
- $just(text, align=r, indent=2)
- `$just(text, width=40)`
- `$just(text, align=r, indent=2)`
"""
if not args:
@ -925,9 +927,9 @@ def funcparser_callable_clr(*args, **kwargs):
color (str, optional): If given,
Example:
- `$clr(r, text, n)`
- `$clr(r, text)`
- `$clr(text, start=r, end=n)`
- `$clr(r, text, n) -> "|rtext|n"`
- `$clr(r, text) -> "|rtext|n`
- `$clr(text, start=r, end=n) -> "|rtext|n"`
"""
if not args:
@ -961,7 +963,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
Args:
query (str): The key or dbref to search for.
Kwargs:
Keyword Args:
return_list (bool): If set, return a list of objects with
0, 1 or more matches to `query`. Defaults to False.
type (str): One of 'obj', 'account', 'script'
@ -1037,7 +1039,7 @@ def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, cap
Replaces with you for the caller of the string, with the display_name
of the caller for others.
Kwargs:
Keyword Args:
caller (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.
@ -1093,10 +1095,9 @@ def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capita
def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
"""
$conj(verb)
Conjugate a verb according to if it should be 2nd or third person.
Kwargs:
Keyword Args:
caller (Object): The object who represents 'you' in the string.
receiver (Object): The recipient of the string.
@ -1107,13 +1108,12 @@ def funcparser_callable_conjugate(*args, caller=None, receiver=None, **kwargs):
ParsingError: If `you` and `recipient` were not both supplied.
Notes:
Note that it will not capitalized.
This assumes that the active party (You) is the one performing the verb.
Note that the verb will not be capitalized. It also
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.
than 'you'. The caller/receiver must be passed to the parser directly.
Exampels:
Examples:
This is often used in combination with the $you/You( callables.
- `With a grin, $you() $conj(jump)`

View file

@ -348,18 +348,19 @@ class TestDefaultCallables(TestCase):
def test_random(self):
string = "$random(1,10)"
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(1 <= ret <= 10)
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(1 <= ret <= 10)
string = "$random()"
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(0 <= ret <= 1)
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(0 <= ret <= 1)
string = "$random(1.0, 3.0)"
for i in range(1000):
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(isinstance(ret, float))
print("ret:", ret)
self.assertTrue(1.0 <= ret <= 3.0)
def test_randint(self):

View file

@ -1080,7 +1080,8 @@ def repeat(interval, callback, persistent=True, idstring="", stop=False,
store_key (tuple, optional): This is only used in combination with `stop` and
should be the return given from the original `repeat` call. If this
is given, all other args except `stop` are ignored.
*args, **kwargs: Used as arguments to `callback`.
*args: Used as arguments to `callback`.
**kwargs: Keyword-arguments to pass to `callback`.
Returns:
tuple or None: The tuple is the `store_key` - the identifier for the