"""
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."
```
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'
function for the parser to accept it. This is usually done in a module with
regular Python functions on the form:
```python
# in a module whose path is passed to the parser
def _helper(x):
# use underscore to NOT make the function available as a callable
def funcname(*args, **kwargs):
# this can be accessed as $funcname(*args, **kwargs)
# it must always accept *args and **kwargs.
...
return something
```
Usage:
```python
from evennia.utils.funcparser import FuncParser
parser = FuncParser("path.to.module_with_callables")
result = parser.parse("String with $funcname() in it")
```
The `FuncParser` also accepts a direct dict mapping of `{'name': callable, ...}`.
---
"""
import dataclasses
import inspect
import random
from django.conf import settings
from evennia.utils import logger, search
from evennia.utils.utils import (
callables_from_module,
crop,
int2str,
justify,
make_iter,
pad,
safe_convert_to_types,
variable_from_module,
)
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
from evennia.utils.verb_conjugation.pronouns import pronoun_to_viewpoints
# setup
_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:
"""
Represents a function parsed from the string
"""
prefix: str = _START_CHAR
funcname: str = ""
args: list = dataclasses.field(default_factory=list)
kwargs: dict = dataclasses.field(default_factory=dict)
# state storage
fullstr: str = ""
infuncstr: str = ""
rawstr: str = ""
double_quoted: int = -1
current_kwarg: str = ""
open_lparens: int = 0
open_lsquate: int = 0
open_lcurly: int = 0
exec_return = ""
def get(self):
return self.funcname, self.args, self.kwargs
def __str__(self):
return self.prefix + self.rawstr + self.infuncstr
[docs]class ParsingError(RuntimeError):
"""
Failed to parse for some reason.
"""
pass
[docs]class FuncParser:
"""
Sets up a parser for strings containing `$funcname(*args, **kwargs)`
substrings.
"""
[docs] def __init__(
self,
callables,
start_char=_START_CHAR,
escape_char=_ESCAPE_CHAR,
max_nesting=_MAX_NESTING,
**default_kwargs,
):
"""
Initialize the parser.
Args:
callables (str, module, list or dict): Where to find
'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 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*
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
into `.parse` as `**reserved_kwargs` instead.
"""
if isinstance(callables, dict):
loaded_callables = {**callables}
else:
# load all modules/paths in sequence. Later-added will override
# earlier same-named callables (allows for overriding evennia defaults)
loaded_callables = {}
for module_or_path in make_iter(callables):
callables_mapping = variable_from_module(
module_or_path, variable="FUNCPARSER_CALLABLES"
)
if callables_mapping:
try:
# mapping supplied in variable
loaded_callables.update(callables_mapping)
except ValueError:
raise ParsingError(
f"Failure to parse - {module_or_path}.FUNCPARSER_CALLABLES "
"(must be a dict {'funcname': callable, ...})"
)
else:
# use all top-level variables
# (handles both paths and module instances
loaded_callables.update(callables_from_module(module_or_path))
self.validate_callables(loaded_callables)
self.callables = loaded_callables
self.escape_char = escape_char
self.start_char = start_char
self.default_kwargs = default_kwargs
[docs] def validate_callables(self, callables):
"""
Validate the loaded callables. Each callable must support at least
`funcname(*args, **kwargs)`.
property.
Args:
callables (dict): A mapping `{"funcname": callable, ...}` to validate
Raise:
AssertionError: If invalid callable was found.
Notes:
This is also a good method to override for individual parsers
needing to run any particular pre-checks.
"""
for funcname, clble in callables.items():
try:
mapping = inspect.getfullargspec(clble)
except TypeError:
logger.log_trace(f"Could not run getfullargspec on {funcname}: {clble}")
else:
assert mapping.varargs, f"Parse-func callable '{funcname}' does not support *args."
assert mapping.varkw, f"Parse-func callable '{funcname}' does not support **kwargs."
[docs] def execute(self, parsedfunc, raise_errors=False, **reserved_kwargs):
"""
Execute a parsed function
Args:
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.
**reserved_kwargs: These kwargs are _guaranteed_ to always be passed into
the callable on every call. It will override any default kwargs
_and_ also a same-named kwarg given manually in the $funcname
call. This is often used by Evennia to pass required data into
the callable, for example the current Session for inlinefuncs.
Returns:
any: The result of the execution. If this is a nested function, it
can be anything, otherwise it will be converted to a string later.
Always a string on un-raised error (the unparsed function string).
Raises:
ParsingError, any: A `ParsingError` if the function could not be
found, otherwise error from function definition. Only raised if
`raise_errors` is `True`
Notes:
The kwargs passed into the callable will be a mixture of the
`default_kwargs` passed into `FuncParser.__init__`, kwargs given
directly in the `$funcdef` string, and the `reserved_kwargs` this
function gets from `.parse()`. For colliding keys, funcdef-defined
kwargs will override default kwargs while reserved kwargs will always
override the other two.
"""
funcname, args, kwargs = parsedfunc.get()
func = self.callables.get(funcname)
if not func:
if raise_errors:
available = ", ".join(f"'{key}'" for key in self.callables)
raise ParsingError(
f"Unknown parsed function '{str(parsedfunc)}' (available: {available})"
)
return str(parsedfunc)
# build kwargs in the proper priority order
kwargs = {
**self.default_kwargs,
**kwargs,
**reserved_kwargs,
**{"funcparser": self, "raise_errors": raise_errors},
}
try:
ret = func(*args, **kwargs)
return ret
except ParsingError:
if raise_errors:
raise
return str(parsedfunc)
except Exception:
logger.log_trace()
if raise_errors:
raise
return str(parsedfunc)
[docs] 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.
Args:
string (str): The string to parse.
raise_errors (bool, optional): By default, a failing parse just
means not parsing the string but leaving it as-is. If this is
`True`, errors (like not closing brackets) will lead to an
ParsingError.
escape (bool, optional): If set, escape all found functions so they
are not executed by later parsing.
strip (bool, optional): If set, strip any inline funcs from string
as if they were not there.
return_str (bool, optional): If set (default), always convert the
parse result to a string, otherwise return the result of the
latest called inlinefunc (if called separately).
**reserved_kwargs: If given, these are guaranteed to _always_ pass
as part of each parsed callable's **kwargs. These override
same-named default options given in `__init__` as well as any
same-named kwarg given in the string function. This is because
it is often used by Evennia to pass necessary kwargs into each
callable (like the current Session object for inlinefuncs).
Returns:
str or any: The parsed string, or the same string on error (if
`raise_errors` is `False`). This is always a string
Raises:
ParsingError: If a problem is encountered and `raise_errors` is True.
"""
start_char = self.start_char
escape_char = self.escape_char
# replace e.g. $$ with \$ so we only need to handle one escape method
string = string.replace(start_char + start_char, escape_char + start_char)
# parsing state
callstack = []
double_quoted = -1
open_lparens = 0 # open (
open_lsquare = 0 # open [
open_lcurly = 0 # open {
escaped = False
current_kwarg = ""
exec_return = ""
curr_func = None
fullstr = "" # final string
infuncstr = "" # string parts inside the current level of $funcdef (including $)
literal_infuncstr = False
for char in string:
if escaped:
# always store escaped characters verbatim
if curr_func:
infuncstr += char
else:
fullstr += char
escaped = False
continue
if char == escape_char:
# don't store the escape-char itself
escaped = True
continue
if char == start_char:
# start a new function definition (not escaped as $$)
if curr_func:
# we are starting a nested funcdef
if len(callstack) >= _MAX_NESTING - 1:
# stack full - ignore this function
if raise_errors:
raise ParsingError(
"Only allows for parsing nesting function defs "
f"to a max depth of {_MAX_NESTING}."
)
infuncstr += char
continue
else:
# store state for the current func and stack it
curr_func.current_kwarg = current_kwarg
curr_func.infuncstr = infuncstr
curr_func.double_quoted = double_quoted
curr_func.open_lparens = open_lparens
curr_func.open_lsquare = open_lsquare
curr_func.open_lcurly = open_lcurly
# we must strip the remaining funcstr so it's not counted twice
curr_func.rawstr = curr_func.rawstr[: -len(infuncstr)]
current_kwarg = ""
infuncstr = ""
double_quoted = -1
open_lparens = 0
open_lsquare = 0
open_lcurly = 0
exec_return = ""
literal_infuncstr = False
callstack.append(curr_func)
# start a new func
curr_func = _ParsedFunc(prefix=char, fullstr=char)
continue
if not curr_func:
# a normal piece of string
fullstr += char
# this must always be a string
return_str = True
continue
# in a function def (can be nested)
curr_func.rawstr += char
if exec_return != "" and char not in (",=)"):
# if exec_return is followed by any other character
# than one demarking an arg,kwarg or function-end
# it must immediately merge as a string
infuncstr += str(exec_return)
exec_return = ""
if char == '"': # note that this is the same as '\"'
# a double quote = flip status
if double_quoted == 0:
infuncstr = infuncstr[1:]
double_quoted = -1
elif double_quoted > 0:
prefix = infuncstr[0:double_quoted]
infuncstr = prefix + infuncstr[double_quoted + 1 :]
double_quoted = -1
else:
infuncstr += char
infuncstr = infuncstr.strip()
double_quoted = len(infuncstr) - 1
literal_infuncstr = True
continue
if double_quoted >= 0:
# inside a string definition - this escapes everything else
infuncstr += char
continue
# special characters detected inside function def
if char == "(":
if not curr_func.funcname:
# end of a funcdef name
curr_func.funcname = infuncstr
curr_func.fullstr += infuncstr + char
infuncstr = ""
else:
# just a random left-parenthesis
infuncstr += char
# track the open left-parenthesis
open_lparens += 1
continue
if char in "[]":
# a square bracket - start/end of a list?
infuncstr += char
open_lsquare += -1 if char == "]" else 1
continue
if char in "{}":
# a curly bracket - start/end of dict/set?
infuncstr += char
open_lcurly += -1 if char == "}" else 1
continue
if char == "=":
# beginning of a keyword argument
if exec_return != "":
infuncstr = exec_return
current_kwarg = infuncstr.strip()
curr_func.kwargs[current_kwarg] = ""
curr_func.fullstr += infuncstr + char
infuncstr = ""
continue
if char in (",)"):
# commas and right-parens may indicate arguments ending
if open_lparens > 1:
# one open left-parens is ok (beginning of arglist), more
# indicate we are inside an unclosed, nested (, so
# we need to not count this as a new arg or end of funcdef.
infuncstr += char
open_lparens -= 1 if char == ")" else 0
continue
if open_lcurly > 0 or open_lsquare > 0:
# also escape inside an open [... or {... structure
infuncstr += char
continue
if exec_return != "":
# store the execution return as-received
if current_kwarg:
curr_func.kwargs[current_kwarg] = exec_return
else:
curr_func.args.append(exec_return)
else:
if not literal_infuncstr:
infuncstr = infuncstr.strip()
# store a string instead
if current_kwarg:
curr_func.kwargs[current_kwarg] = infuncstr
elif literal_infuncstr or infuncstr.strip():
# don't store the empty string
curr_func.args.append(infuncstr)
# note that at this point either exec_return or infuncstr will
# be empty. We need to store the full string so we can print
# it 'raw' in case this funcdef turns out to e.g. lack an
# ending paranthesis
curr_func.fullstr += str(exec_return) + infuncstr + char
current_kwarg = ""
exec_return = ""
infuncstr = ""
literal_infuncstr = False
if char == ")":
# closing the function list - this means we have a
# ready function-def to run.
open_lparens = 0
if strip:
# remove function as if it returned empty
exec_return = ""
elif escape:
# get function and set it as escaped
exec_return = escape_char + curr_func.fullstr
else:
# execute the function - the result may be a string or
# something else
exec_return = self.execute(
curr_func, raise_errors=raise_errors, **reserved_kwargs
)
if callstack:
# unnest the higher-level funcdef from stack
# and continue where we were
curr_func = callstack.pop()
current_kwarg = curr_func.current_kwarg
if curr_func.infuncstr:
# if we have an ongoing string, we must merge the
# exec into this as a part of that string
infuncstr = curr_func.infuncstr + str(exec_return)
exec_return = ""
curr_func.infuncstr = ""
double_quoted = curr_func.double_quoted
open_lparens = curr_func.open_lparens
open_lsquare = curr_func.open_lsquare
open_lcurly = curr_func.open_lcurly
else:
# back to the top-level string - this means the
# exec_return should always be converted to a string.
curr_func = None
fullstr += str(exec_return)
if return_str:
exec_return = ""
infuncstr = ""
literal_infuncstr = False
continue
infuncstr += char
if curr_func:
# if there is a still open funcdef or defs remaining in callstack,
# these are malformed (no closing bracket) and we should get their
# strings as-is.
callstack.append(curr_func)
for inum, _ in enumerate(range(len(callstack))):
funcstr = str(callstack.pop())
if inum == 0 and funcstr.endswith(infuncstr):
# avoid double-echo of nested function calls. This should
# produce a good result most of the time, but it's not 100%
# guaranteed to, since it can ignore genuine duplicates
infuncstr = funcstr
else:
infuncstr = funcstr + infuncstr
if not return_str and exec_return != "":
# return explicit return
return exec_return
# add the last bit to the finished string
fullstr += infuncstr
return fullstr
[docs] def parse_to_any(
self, string, raise_errors=False, escape=False, strip=False, **reserved_kwargs
):
"""
This parses a string and if the string only contains a "$func(...)",
the return will be the return value of that function, even if it's not
a string. If mixed in with other strings, the result will still always
be a string.
Args:
string (str): The string to parse.
raise_errors (bool, optional): If unset, leave a failing (or
unrecognized) inline function as unparsed in the string. If set,
raise an ParsingError.
escape (bool, optional): If set, escape all found functions so they
are not executed by later parsing.
strip (bool, optional): If set, strip any inline funcs from string
as if they were not there.
**reserved_kwargs: If given, these are guaranteed to _always_ pass
as part of each parsed callable's **kwargs. These override
same-named default options given in `__init__` as well as any
same-named kwarg given in the string function. This is because
it is often used by Evennia to pass necessary kwargs into each
callable (like the current Session object for inlinefuncs).
Returns:
any: The return from the callable. Or string if the callable is not
given alone in the string.
Raises:
ParsingError: If a problem is encountered and `raise_errors` is True.
Notes:
This is a convenience wrapper for `self.parse(..., return_str=False)` which
accomplishes the same thing.
Examples:
::
from ast import literal_eval
from evennia.utils.funcparser import FuncParser
def ret1(*args, **kwargs):
return 1
parser = FuncParser({"lit": lit})
assert parser.parse_to_any("$ret1()" == 1
assert parser.parse_to_any("$ret1() and text" == '1 and text'
"""
return self.parse(
string,
raise_errors=raise_errors,
escape=escape,
strip=strip,
return_str=False,
**reserved_kwargs,
)
#
# Default funcparser callables. These are made available from this module's
# FUNCPARSER_CALLABLES.
#
[docs]def funcparser_callable_eval(*args, **kwargs):
"""
Funcparser callable. This will combine safe evaluations to try to parse the
incoming string into a python object. If it fails, the return will be same
as the input.
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.
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)
return args[0] if args else ""
[docs]def funcparser_callable_toint(*args, **kwargs):
"""Usage: $toint(43.0) -> 43"""
inp = funcparser_callable_eval(*args, **kwargs)
try:
return int(inp)
except TypeError:
return inp
except ValueError:
return inp
[docs]def funcparser_callable_int2str(*args, **kwargs):
"""
Usage: $int2str(1) -> 'one' etc, up to 12->twelve.
Args:
number (int): The number. If not an int, will be converted.
Uses the int2str utility function.
"""
if not args:
return ""
try:
number = int(args[0])
except ValueError:
return args[0]
return int2str(number)
[docs]def funcparser_callable_an(*args, **kwargs):
"""
Usage: $an(thing) -> a thing
Adds a/an depending on if the first letter of the given word is a consonant or not.
"""
if not args:
return ""
item = str(args[0])
if item and item[0] in "aeiouyAEIOUY":
return f"an {item}"
return f"a {item}"
def _apply_operation_two_elements(*args, operator="+", **kwargs):
"""
Helper operating on two arguments
Args:
val1 (any): First value to operate on.
val2 (any): Second value to operate on.
Return:
any: The result of val1 + val2. Values must be
valid simple Python structures possible to add,
such as numbers, lists etc. The $eval is usually
better for non-list arithmetic.
"""
args, kwargs = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)
if not len(args) > 1:
return ""
val1, val2 = args[0], args[1]
try:
if operator == "+":
return val1 + val2
elif operator == "-":
return val1 - val2
elif operator == "*":
return val1 * val2
elif operator == "/":
return val1 / val2
except Exception:
if kwargs.get("raise_errors"):
raise
return ""
[docs]def funcparser_callable_add(*args, **kwargs):
"""Usage: `$add(val1, val2) -> val1 + val2`"""
return _apply_operation_two_elements(*args, operator="+", **kwargs)
[docs]def funcparser_callable_sub(*args, **kwargs):
"""Usage: ``$sub(val1, val2) -> val1 - val2`"""
return _apply_operation_two_elements(*args, operator="-", **kwargs)
[docs]def funcparser_callable_mult(*args, **kwargs):
"""Usage: `$mult(val1, val2) -> val1 * val2`"""
return _apply_operation_two_elements(*args, operator="*", **kwargs)
[docs]def funcparser_callable_div(*args, **kwargs):
"""Usage: `$mult(val1, val2) -> val1 / val2`"""
return _apply_operation_two_elements(*args, operator="/", **kwargs)
[docs]def funcparser_callable_round(*args, **kwargs):
"""
Funcparser callable. Rounds an incoming float to a
certain number of significant digits.
Args:
inp (str or number): If a string, it will attempt
to be converted to a number first.
significant (int): The number of significant digits. Default is None -
this will turn the result into an int.
Returns:
any: The rounded value or inp if inp was not a number.
Examples:
- `$round(3.5434343, 3) -> 3.543`
- `$round($random(), 2)` - rounds random result, e.g `0.22`
"""
if not args:
return ""
args, _ = safe_convert_to_types(((float, int), {}), *args, **kwargs)
num, *significant = args
significant = significant[0] if significant else 0
try:
return round(num, significant)
except Exception:
if kwargs.get("raise_errors"):
raise
return ""
[docs]def funcparser_callable_random(*args, **kwargs):
"""
Funcparser callable. 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.
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.
Examples:
- `$random()` - random value [0 .. 1) (float).
- `$random(5)` - random value [0..5] (int)
- `$random(5.0)` - random value [0..5] (float)
- `$random(5, 10)` - random value [5..10] (int)
- `$random(5, 10.0)` - random value [5..10] (float)
"""
args, _ = safe_convert_to_types((("py", "py"), {}), *args, **kwargs)
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
try:
if isinstance(minval, float) or isinstance(maxval, float):
return minval + ((maxval - minval) * random.random())
else:
return random.randint(minval, maxval)
except Exception:
if kwargs.get("raise_errors"):
raise
return ""
[docs]def funcparser_callable_randint(*args, **kwargs):
"""
Usage: $randint(start, end):
Legacy alias - always returns integers.
"""
return int(funcparser_callable_random(*args, **kwargs))
[docs]def funcparser_callable_choice(*args, **kwargs):
"""
FuncParser callable. Picks a random choice from a list.
Args:
listing (list): A list of items to randomly choose between.
This will be converted from a string to a real list.
*args: If multiple args are given, will pick one randomly from them.
Returns:
any: The randomly chosen element.
Example:
- `$choice(key, flower, house)`
- `$choice([1, 2, 3, 4])`
"""
if not args:
return ""
nargs = len(args)
if nargs == 1:
# this needs to be a list/tuple for this to make sense
args, _ = safe_convert_to_types(("py", {}), args[0], **kwargs)
args = make_iter(args[0]) if args else None
else:
# separate arg per entry
converters = ["py" for _ in range(nargs)]
args, _ = safe_convert_to_types((converters, {}), *args, **kwargs)
if not args:
return ""
try:
return random.choice(args)
except Exception:
if kwargs.get("raise_errors"):
raise
return ""
[docs]def funcparser_callable_pad(*args, **kwargs):
"""
FuncParser callable. Pads text to given width, optionally with fill-characters
Args:
text (str): Text to pad.
width (int): 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.
Example:
- `$pad(text, 12, r, ' ') -> " text"`
- `$pad(text, width=12, align=c, fillchar=-) -> "----text----"`
"""
if not args:
return ""
args, kwargs = safe_convert_to_types(
((str, int, str, str), {"width": int, "align": str, "fillchar": str}), *args, **kwargs
)
text, *rest = args
nrest = len(rest)
try:
width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH))
except (TypeError, ValueError):
width = _CLIENT_DEFAULT_WIDTH
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)
[docs]def funcparser_callable_crop(*args, **kwargs):
"""
FuncParser callable. 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 `[...]`.
Example:
- `$crop(A long text, 10, [...]) -> "A lon[...]"`
- `$crop(text, width=11, suffix='[...]) -> "A long[...]"`
"""
if not args:
return ""
text, *rest = args
nrest = len(rest)
try:
width = int(kwargs.get("width", rest[0] if nrest > 0 else _CLIENT_DEFAULT_WIDTH))
except (TypeError, ValueError):
width = _CLIENT_DEFAULT_WIDTH
suffix = kwargs.get("suffix", rest[1] if nrest > 1 else "[...]")
return crop(str(text), width=width, suffix=str(suffix))
[docs]def funcparser_callable_space(*args, **kwarg):
"""
Usage: $space(43)
Insert a length of space.
"""
if not args:
return ""
try:
width = int(args[0])
except ValueError:
width = 1
return " " * width
[docs]def funcparser_callable_justify(*args, **kwargs):
"""
Justify text across a width, default across screen width.
Args:
text (str): Text to justify.
width (int, optional): Defaults to default screen width.
align (str, optional): One of 'l', 'c', 'r' or 'f' for 'full'.
indent (int, optional): Intendation of text block, if any.
Returns:
str: The justified text.
Examples:
- `$just(text, width=40)`
- `$just(text, align=r, indent=2)`
"""
if not args:
return ""
text, *rest = args
lrest = len(rest)
try:
width = int(kwargs.get("width", rest[0] if lrest > 0 else _CLIENT_DEFAULT_WIDTH))
except (TypeError, ValueError):
width = _CLIENT_DEFAULT_WIDTH
align = str(kwargs.get("align", rest[1] if lrest > 1 else "f"))
try:
indent = int(kwargs.get("indent", rest[2] if lrest > 2 else 0))
except (TypeError, ValueError):
indent = 0
return justify(str(text), width=width, align=align, indent=indent)
# legacy for backwards compatibility
[docs]def funcparser_callable_left_justify(*args, **kwargs):
"Usage: $ljust(text)"
return funcparser_callable_justify(*args, align="l", **kwargs)
[docs]def funcparser_callable_right_justify(*args, **kwargs):
"Usage: $rjust(text)"
return funcparser_callable_justify(*args, align="r", **kwargs)
[docs]def funcparser_callable_center_justify(*args, **kwargs):
"Usage: $cjust(text)"
return funcparser_callable_justify(*args, align="c", **kwargs)
[docs]def funcparser_callable_clr(*args, **kwargs):
"""
FuncParser callable. 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).
Kwargs:
color (str, optional): If given,
Example:
- `$clr(r, text, n) -> "|rtext|n"`
- `$clr(r, text) -> "|rtext|n`
- `$clr(text, start=r, end=n) -> "|rtext|n"`
"""
if not args:
return ""
startclr, text, endclr = "", "", ""
if len(args) > 1:
# $clr(pre, text, post))
startclr, *rest = args
if rest:
text, *endclr = rest
if endclr:
endclr = endclr[0]
else:
# $clr(text, start=pre, end=post)
text = args[0]
startclr = kwargs.get("start", "")
endclr = kwargs.get("end", "")
startclr = "|" + startclr if startclr else ""
endclr = "|" + endclr if endclr else ("|n" if startclr else "")
return f"{startclr}{text}{endclr}"
[docs]def funcparser_callable_pluralize(*args, **kwargs):
"""
FuncParser callable. Handles pluralization of a word.
Args:
singular_word (str): The base (singular) word to optionally pluralize
number (int): The number of elements; if 1 (or 0), use `singular_word` as-is,
otherwise use plural form.
plural_word (str, optional): If given, this will be used if `number`
is greater than one. If not given, we simply add 's' to the end of
`singular_word`.
Example:
- `$pluralize(thing, 2)` -> "things"
- `$pluralize(goose, 18, geese)` -> "geese"
"""
if not args:
return ""
nargs = len(args)
if nargs > 2:
singular_word, number, plural_word = args[:3]
elif nargs > 1:
singular_word, number = args[:2]
plural_word = f"{singular_word}s"
else:
singular_word, number = args[0], 1
return singular_word if abs(int(number)) in (0, 1) else plural_word
[docs]def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
"""
FuncParser callable. Finds an object based on name or #dbref. Note that
this requries the parser be called with the caller's Session for proper
security. If called without session, the call is aborted.
Args:
query (str): The key or dbref to search for. This can consist of any args used
for one of the regular search methods. Also kwargs will be passed into
the search (except the kwargs given below)
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'
caller (Entity): Supplied to Parser. This is required and will
be passed into the access check for the entity being searched for.
The 'control' permission is required.
access (str): Which locktype access to check. Unset to disable the
security check.
**kwargs: Will be passed into the main search.
Returns:
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, or caller was not
passed into parser.
Examples:
- "$search(#233)"
- "$search(Tom, type=account)"
- "$search(meadow, return_list=True)"
- "$search(beach, category=outdoors, type=tag)
"""
# clean out funcparser-specific kwargs so we can use the kwargs for
# searching
search_kwargs = {
key: value
for key, value in kwargs.items()
if key not in ("funcparser", "raise_errors", "type", "return_list")
}
return_list = str(kwargs.pop("return_list", "false")).lower() == "true"
if not args:
return [] if return_list else None
if not caller:
raise ParsingError("$search requires a `caller` passed to the parser.")
typ = kwargs.get("type", "obj")
targets = []
if typ == "obj":
targets = search.search_object(*args, **search_kwargs)
elif typ == "account":
targets = search.search_account(*args, **search_kwargs)
elif typ == "script":
targets = search.search_script(*args, **search_kwargs)
elif typ == "tag":
targets = search.search_object_by_tag(*args, **search_kwargs)
if not targets:
if return_list:
return []
raise ParsingError(f"$search: Query '{args[0]}' gave no matches.")
if len(targets) > 1 and not return_list:
raise ParsingError(
"$search: Query '{query}' found {num} matches. "
"Set return_list=True to accept a list".format(query=query, num=len(targets))
)
for target in targets:
if not target.access(caller, access):
raise ParsingError("$search Cannot add found entity - access failure.")
return list(targets) if return_list else targets[0]
[docs]def funcparser_callable_search_list(*args, caller=None, access="control", **kwargs):
"""
Usage: $objlist(#123)
Legacy alias for search with a return_list=True kwarg preset.
"""
return funcparser_callable_search(
*args, caller=caller, access=access, return_list=True, **kwargs
)
[docs]def funcparser_callable_you(
*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
"""
Usage: $you() or $you(key)
Replaces with you for the caller of the string, with the display_name
of the caller for others.
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.
mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
used to find which object `$you(key)` refers to. If not given, the
`caller` kwarg is used.
capitalize (bool): Passed by the You helper, to capitalize you.
Returns:
str: The parsed string.
Raises:
ParsingError: If `caller` and `receiver` were not supplied.
Notes:
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 $conj() callable.
- `With a grin, $you() $conj(jump) at $you(tommy).`
The caller-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 args and mapping:
# this would mean a $you(key) form
caller = mapping.get(args[0], None)
if not (caller and receiver):
raise ParsingError("No caller or receiver supplied to $you callable.")
capitalize = bool(capitalize)
if caller == receiver:
return "You" if capitalize else "you"
return (
caller.get_display_name(looker=receiver)
if hasattr(caller, "get_display_name")
else str(caller)
)
[docs]def funcparser_callable_you_capitalize(
*args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs
):
"""
Usage: $You() - capitalizes the 'you' output.
"""
return funcparser_callable_you(
*args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs
)
[docs]def funcparser_callable_your(
*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
"""
Usage: $your() or $your(key)
Replaces with your for the caller of the string, with the display_name +'s
of the caller for others.
Keyword Args:
caller (Object): The 'your' in the string. This is used unless another
your-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
`caller` kwarg is used.
capitalize (bool): Passed by the You helper, to capitalize you.
Returns:
str: The parsed string.
Raises:
ParsingError: If `caller` and `receiver` were not supplied.
Notes:
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.
- `$your() pet jumps at $you(tommy).`
The caller-object will see "Your pet jumps Tommy."
Tommy will see "CharName's pet jumps at you."
Others will see "CharName's pet jumps at Tommy."
"""
if args and mapping:
# this would mean a $your(key) form
caller = mapping.get(args[0], None)
if not (caller and receiver):
raise ParsingError("No caller or receiver supplied to $your callable.")
capitalize = bool(capitalize)
if caller == receiver:
return "Your" if capitalize else "your"
name = (
caller.get_display_name(looker=receiver)
if hasattr(caller, "get_display_name")
else str(caller)
)
return name + "'s"
[docs]def funcparser_callable_your_capitalize(
*args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs
):
"""
Usage: $Your() - capitalizes the 'your' output.
"""
return funcparser_callable_your(
*args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs
)
[docs]def funcparser_callable_conjugate(*args, caller=None, receiver=None, mapping=None, **kwargs):
"""
Usage: $conj(word, [key])
Conjugate a verb according to if it should be 2nd or third person.
Keyword Args:
caller (Object): The object who represents 'you' in the string.
receiver (Object): The recipient of the string.
mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
used to find which object the optional `key` argument refers to. If not given,
the `caller` kwarg is used.
Returns:
str: The parsed string.
Raises:
ParsingError: If `you` and `recipient` were not both supplied.
Notes:
Note that the verb will not be capitalized.
Examples:
This is often used in combination with the $you/You callables.
- `With a grin, $you() $conj(jump)`
You will see "With a grin, you jump."
Others will see "With a grin, CharName jumps."
"""
if not args:
return ""
if not (caller and receiver):
raise ParsingError("No caller/receiver supplied to $conj callable")
verb, *options = args
obj = caller
if mapping and options:
# get the correct referenced object from the mapping, or fall back to caller
obj = mapping.get(options[0], caller)
second_person_str, third_person_str = verb_actor_stance_components(verb)
return second_person_str if obj == receiver else third_person_str
[docs]def funcparser_callable_conjugate_for_pronouns(
*args, caller=None, receiver=None, mapping=None, **kwargs
):
"""
Usage: $pconj(word, [key])
Conjugate a verb according to if it should be 2nd or third person, respecting the
singular/plural gendering for third person.
Keyword Args:
caller (Object): The object who represents 'you' in the string.
receiver (Object): The recipient of the string.
mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
used to find which object the optional `key` argument refers to. If not given,
the `caller` kwarg is used.
Returns:
str: The parsed string.
Raises:
ParsingError: If `you` and `recipient` were not both supplied.
Notes:
Note that the verb will not be capitalized.
Examples:
This is often used in combination with the $pron/Pron callables.
- `With a grin, $pron(you) $pconj(jump)`
You will see "With a grin, you jump."
With your gender as "male", others will see "With a grin, he jumps."
With your gender as "plural", others will see "With a grin, they jump."
"""
if not args:
return ""
if not (caller and receiver):
raise ParsingError("No caller/receiver supplied to $conj callable")
verb, *options = args
obj = caller
if mapping and options:
# get the correct referenced object from the mapping, or fall back to caller
obj = mapping.get(options[0], caller)
# identify whether the 3rd person form should be singular or plural
plural = False
if hasattr(obj, "gender"):
if callable(obj.gender):
plural = obj.gender() == "plural"
else:
plural = obj.gender == "plural"
second_person_str, third_person_str = verb_actor_stance_components(verb, plural=plural)
return second_person_str if obj == receiver else third_person_str
[docs]def funcparser_callable_pronoun(
*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs
):
"""
Usage: $pron(word, [options], [key])
Adjust pronouns to the expected form. Pronouns are words you use instead of a
proper name, such as 'him', 'herself', 'theirs' etc. These look different
depending on who sees the outgoing string.
The parser maps between this table ...
==================== ======= ======= ========== ========== ===========
1st/2nd person Subject Object Possessive Possessive Reflexive
Pronoun Pronoun Adjective Pronoun Pronoun
==================== ======= ======= ========== ========== ===========
1st person I me my mine myself
1st person plural we us our ours ourselves
2nd person you you your yours yourself
2nd person plural you you your yours yourselves
==================== ======= ======= ========== ========== ===========
... and this table (and vice versa).
==================== ======= ======= ========== ========== ===========
3rd person Subject Object Possessive Possessive Reflexive
Pronoun Pronoun Adjective Pronoun Pronoun
==================== ======= ======= ========== ========== ===========
3rd person male he him his his himself
3rd person female she her her hers herself
3rd person neutral it it its itself
3rd person plural they them their theirs themselves
==================== ======= ======= ========== ========== ===========
This system will examine `caller` for either a property or a callable `.gender` to
get a default gender fallback (if not specified in the call). If a callable,
`.gender` will be called without arguments and should return a string
`male`/`female`/`neutral`/`plural` (plural is considered a gender for this purpose).
If no `gender` property/callable is found, `neutral` is used as a fallback.
The pronoun-type default (if not specified in call) is `subject pronoun`.
Args:
pronoun (str): Input argument to parsed call. This can be any of the pronouns
in the table above. If given in 1st/second form, they will be mappped to
3rd-person form for others viewing the message (but will need extra input
via the `gender`, see below). If given on 3rd person form, this will be
mapped to 2nd person form for `caller` unless `viewpoint` is specified
in options.
options (str, optional): A space- or comma-separated string detailing `pronoun_type`,
`gender`/`plural` and/or `viewpoint` to help the mapper differentiate between
non-unique cases (such as if `you` should become `him` or `they`).
Allowed values are:
- `subject pronoun`/`subject`/`sp` (I, you, he, they)
- `object pronoun`/`object/`/`op` (me, you, him, them)
- `possessive adjective`/`adjective`/`pa` (my, your, his, their)
- `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs)
- `male`/`m`
- `female`/`f`
- `neutral`/`n`
- `plural`/`p`
- `1st person`/`1st`/`1`
- `2nd person`/`2nd`/`2`
- `3rd person`/`3rd`/`3`
key (str, optional): If a mapping is provided, a string defining which object to
reference when finding the correct pronoun. If not provided, it defaults
to `caller`
Keyword Args:
caller (Object): The object creating the string. If this has a property 'gender',
it will be checked for a string 'male/female/neutral' to determine
the 3rd person gender (but if `pronoun_type` contains a gender
component, that takes precedence). Provided automatically to the
funcparser.
receiver (Object): The recipient of the string. This being the same as
`caller` or not helps determine 2nd vs 3rd-person forms. This is
provided automatically by the funcparser.
mapping (dict, optional): This is a mapping `{key:Object, ...}` and is
used to find which object the optional `key` argument refers to. If not given,
the `caller` kwarg is used.
capitalize (bool): The input retains its capitalization. If this is set the output is
always capitalized.
Examples:
====================== ============= ===========
Input caller sees others see
====================== ============= ===========
$pron(I, m) I he
$pron(you,fo) you her
$pron(yourself) yourself itself
$pron(its) your its
$pron(you,op,p) you them
====================== ============= ===========
Notes:
There is no option to specify reflexive pronouns since they are all unique
and the mapping can always be auto-detected.
"""
if not args:
return ""
# by default, we use the caller as the object being referred to
obj = caller
pronoun, *options = args
if options and mapping:
# check if the last argument is a valid mapping key
if options[-1] in mapping:
# get the object and remove the key from options
obj = mapping[options[-1]]
options = options[:-1]
# options is either multiple args or a space-separated string
if len(options) == 1:
options = options[0]
# default differentiators
default_pronoun_type = "subject pronoun"
default_gender = "neutral"
default_viewpoint = "2nd person"
if hasattr(obj, "gender"):
if callable(obj.gender):
default_gender = obj.gender()
else:
default_gender = obj.gender
if "viewpoint" in kwargs:
# passed into FuncParser initialization
default_viewpoint = kwargs["viewpoint"]
pronoun_1st_or_2nd_person, pronoun_3rd_person = pronoun_to_viewpoints(
pronoun,
options,
pronoun_type=default_pronoun_type,
gender=default_gender,
viewpoint=default_viewpoint,
)
if capitalize:
pronoun_1st_or_2nd_person = pronoun_1st_or_2nd_person.capitalize()
pronoun_3rd_person = pronoun_3rd_person.capitalize()
return pronoun_1st_or_2nd_person if obj == receiver else pronoun_3rd_person
[docs]def funcparser_callable_pronoun_capitalize(
*args, caller=None, receiver=None, capitalize=True, **kwargs
):
"""
Usage: $Pron(word, [options]) - always maps to a capitalized word.
"""
return funcparser_callable_pronoun(
*args, caller=caller, receiver=receiver, capitalize=capitalize, **kwargs
)
# 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,
"sub": funcparser_callable_sub,
"mult": funcparser_callable_mult,
"div": funcparser_callable_div,
"round": funcparser_callable_round,
"toint": funcparser_callable_toint,
# randomizers
"random": funcparser_callable_random,
"randint": funcparser_callable_randint,
"choice": funcparser_callable_choice,
# string manip
"pad": funcparser_callable_pad,
"crop": funcparser_callable_crop,
"just": funcparser_callable_justify,
"ljust": funcparser_callable_left_justify,
"rjust": funcparser_callable_right_justify,
"cjust": funcparser_callable_center_justify,
"justify": funcparser_callable_justify, # aliases for backwards compat
"justify_left": funcparser_callable_left_justify,
"justify_right": funcparser_callable_right_justify,
"justify_center": funcparser_callable_center_justify,
"space": funcparser_callable_space,
"clr": funcparser_callable_clr,
"pluralize": funcparser_callable_pluralize,
"int2str": funcparser_callable_int2str,
"an": funcparser_callable_an,
}
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,
}
ACTOR_STANCE_CALLABLES = {
# requires `you`, `receiver` and `mapping` to be passed into parser
"you": funcparser_callable_you,
"You": funcparser_callable_you_capitalize,
"your": funcparser_callable_your,
"Your": funcparser_callable_your_capitalize,
"obj": funcparser_callable_you,
"Obj": funcparser_callable_you_capitalize,
"conj": funcparser_callable_conjugate,
"pconj": funcparser_callable_conjugate_for_pronouns,
"pron": funcparser_callable_pronoun,
"Pron": funcparser_callable_pronoun_capitalize,
**FUNCPARSER_CALLABLES,
}