Further cleanup and refactoring

This commit is contained in:
Griatch 2021-03-27 19:20:21 +01:00
parent 7891987e05
commit c65c68e4c2
6 changed files with 526 additions and 292 deletions

View file

@ -803,7 +803,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# actor-stance replacements
inmessage = _MSG_CONTENTS_PARSER.parse(
inmessage, raise_errors=True, return_string=True,
you=you, receiver=receiver, mapping=mapping)
caller=you, receiver=receiver, mapping=mapping)
# director-stance replacements
outmessage = inmessage.format(

View file

@ -46,11 +46,10 @@ import inspect
import random
from functools import partial
from django.conf import settings
from ast import literal_eval
from simpleeval import simple_eval
from evennia.utils import logger
from evennia.utils.utils import (
make_iter, callables_from_module, variable_from_module, pad, crop, justify)
make_iter, callables_from_module, variable_from_module, pad, crop, justify,
safe_convert_to_types)
from evennia.utils import search
from evennia.utils.verb_conjugation.conjugate import verb_actor_stance_components
@ -233,11 +232,15 @@ class FuncParser:
f"(available: {available})")
return str(parsedfunc)
nargs = len(args)
# build kwargs in the proper priority order
kwargs = {**self.default_kwargs, **kwargs, **reserved_kwargs}
kwargs = {**self.default_kwargs, **kwargs, **reserved_kwargs,
**{'funcparser': self, "raise_errors": raise_errors}}
try:
return func(*args, **kwargs)
ret = func(*args, **kwargs)
return ret
except ParsingError:
if raise_errors:
raise
@ -601,19 +604,8 @@ def funcparser_callable_eval(*args, **kwargs):
`$py(3 + 4)`
"""
if not args:
return ''
inp = args[0]
if not isinstance(inp, str):
# already converted
return inp
try:
return literal_eval(inp)
except Exception:
try:
return simple_eval(inp)
except Exception:
return inp
args, kwargs = safe_convert_to_types(("py", {}) , *args, **kwargs)
return args[0] if args else ''
def funcparser_callable_toint(*args, **kwargs):
@ -640,28 +632,23 @@ def _apply_operation_two_elements(*args, operator="+", **kwargs):
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 to convert to python structures, otherwise, keep as strings
if isinstance(val1, str):
try:
val1 = literal_eval(val1.strip())
except Exception:
pass
if isinstance(val2, str):
try:
val2 = literal_eval(val2.strip())
except Exception:
pass
if operator == "+":
return val1 + val2
elif operator == "-":
return val1 - val2
elif operator == "*":
return val1 * val2
elif operator == "/":
return val1 / val2
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 ''
def funcparser_callable_add(*args, **kwargs):
@ -705,21 +692,15 @@ def funcparser_callable_round(*args, **kwargs):
"""
if not args:
return ''
inp, *significant = args
significant = significant[0] if significant else '0'
lit_inp = inp
if isinstance(inp, str):
try:
lit_inp = literal_eval(inp)
except Exception:
return inp
args, _ = safe_convert_to_types(((float, int), {}) *args, **kwargs)
num, *significant = args
significant = significant[0] if significant else 0
try:
int(significant)
except Exception:
significant = 0
try:
round(lit_inp, significant)
round(num, significant)
except Exception:
if kwargs.get('raise_errors'):
raise
return ''
def funcparser_callable_random(*args, **kwargs):
@ -744,35 +725,32 @@ def funcparser_callable_random(*args, **kwargs):
- `$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]
minval, maxval = 0, args[0]
elif nargs > 1:
minval, maxval = args[:2]
else:
minval, maxval = ("0", "1")
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 minval + maxval * random.random()
else:
# int mode
try:
minval, maxval = int(minval), int(maxval)
except ValueError:
minval, maxval = 0, 1
return random.randint(minval, maxval)
try:
if isinstance(minval, float) or isinstance(maxval, float):
return minval + maxval * random.random()
else:
return random.randint(minval, maxval)
except Exception:
if kwargs.get('raise_errors'):
raise
return ''
def funcparser_callable_randint(*args, **kwargs):
"""
Usage: $randint(start, end):
Legacy alias - alwas returns integers.
Legacy alias - always returns integers.
"""
return int(funcparser_callable_random(*args, **kwargs))
@ -796,10 +774,13 @@ def funcparser_callable_choice(*args, **kwargs):
"""
if not args:
return ''
inp = args[0]
if not isinstance(inp, str):
inp = literal_eval(inp)
return random.choice(inp)
args, _ = safe_convert_to_types(('py', {}), *args, **kwargs)
try:
return random.choice(args[0])
except Exception:
if kwargs.get('raise_errors'):
raise
return ''
def funcparser_callable_pad(*args, **kwargs):
@ -819,6 +800,9 @@ def funcparser_callable_pad(*args, **kwargs):
"""
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:
@ -833,22 +817,6 @@ def funcparser_callable_pad(*args, **kwargs):
return pad(str(text), width=width, align=align, fillchar=fillchar)
def funcparser_callable_space(*args, **kwarg):
"""
Usage: $space(43)
Insert a length of space.
"""
if not args:
return ''
try:
width = int(args[0])
except TypeError:
width = 1
return " " * width
def funcparser_callable_crop(*args, **kwargs):
"""
FuncParser callable. Crops ingoing text to given widths.
@ -877,6 +845,22 @@ def funcparser_callable_crop(*args, **kwargs):
return crop(str(text), width=width, suffix=str(suffix))
def funcparser_callable_space(*args, **kwarg):
"""
Usage: $space(43)
Insert a length of space.
"""
if not args:
return ''
try:
width = int(args[0])
except TypeError:
width = 1
return " " * width
def funcparser_callable_justify(*args, **kwargs):
"""
Justify text across a width, default across screen width.
@ -948,6 +932,7 @@ def funcparser_callable_clr(*args, **kwargs):
"""
if not args:
return ''
startclr, text, endclr = '', '', ''
if len(args) > 1:
# $clr(pre, text, post))
@ -1045,7 +1030,7 @@ def funcparser_callable_search_list(*args, caller=None, access="control", **kwar
return_list=True, **kwargs)
def funcparser_callable_you(*args, you=None, receiver=None, mapping=None, capitalize=False, **kwargs):
def funcparser_callable_you(*args, caller=None, receiver=None, mapping=None, capitalize=False, **kwargs):
"""
Usage: $you() or $you(key)
@ -1053,19 +1038,19 @@ def funcparser_callable_you(*args, you=None, receiver=None, mapping=None, capita
of the caller for others.
Kwargs:
you (Object): The 'you' in the string. This is used unless another
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
`you` kwarg is used.
`caller` kwarg is used.
capitalize (bool): Passed by the You helper, to capitalize you.
Returns:
str: The parsed string.
Raises:
ParsingError: If `you` and `receiver` were not supplied.
ParsingError: If `caller` and `receiver` were not supplied.
Notes:
The kwargs should be passed the to parser directly.
@ -1076,7 +1061,7 @@ def funcparser_callable_you(*args, you=None, receiver=None, mapping=None, capita
- `With a grin, $you() $conj(jump) at $you(tommy).`
The You-object will see "With a grin, you jump at 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."
@ -1084,17 +1069,17 @@ def funcparser_callable_you(*args, you=None, receiver=None, mapping=None, capita
if args and mapping:
# this would mean a $you(key) form
try:
you = mapping.get(args[0])
caller = mapping.get(args[0])
except KeyError:
pass
if not (you and receiver):
raise ParsingError("No you-object or receiver supplied to $you callable.")
if not (caller and receiver):
raise ParsingError("No caller or receiver supplied to $you callable.")
capitalize = bool(capitalize)
if you == receiver:
if caller == receiver:
return "You" if capitalize else "you"
return you.get_display_name(looker=receiver) if hasattr(you, "get_display_name") else str(you)
return caller.get_display_name(looker=receiver) if hasattr(caller, "get_display_name") else str(caller)
def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capitalize=True, **kwargs):
@ -1106,14 +1091,14 @@ def funcparser_callable_You(*args, you=None, receiver=None, mapping=None, capita
*args, you=you, receiver=receiver, mapping=mapping, capitalize=capitalize, **kwargs)
def funcparser_callable_conjugate(*args, you=None, receiver=None, **kwargs):
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:
you_obj (Object): The object who represents 'you' in the string.
you_target (Object): The recipient of the string.
caller (Object): The object who represents 'you' in the string.
receiver (Object): The recipient of the string.
Returns:
str: The parsed string.
@ -1139,11 +1124,11 @@ def funcparser_callable_conjugate(*args, you=None, receiver=None, **kwargs):
"""
if not args:
return ''
if not (you and receiver):
raise ParsingError("No youj/receiver supplied to $conj callable")
if not (caller and receiver):
raise ParsingError("No caller/receiver supplied to $conj callable")
second_person_str, third_person_str = verb_actor_stance_components(args[0])
return second_person_str if you == receiver else third_person_str
return second_person_str if caller == receiver else third_person_str
# these are made available as callables by adding 'evennia.utils.funcparser' as

View file

@ -14,6 +14,8 @@ from evennia.utils import funcparser, test_resources
def _test_callable(*args, **kwargs):
kwargs.pop('funcparser', None)
kwargs.pop('raise_errors', None)
argstr = ", ".join(args)
kwargstr = ""
if kwargs:
@ -311,10 +313,10 @@ class TestDefaultCallables(TestCase):
"""
mapping = {"char1": self.obj1, "char2": self.obj2}
ret = self.parser.parse(string, you=self.obj1, receiver=self.obj1, mapping=mapping,
ret = self.parser.parse(string, caller=self.obj1, receiver=self.obj1, mapping=mapping,
raise_errors=True)
self.assertEqual(expected_you, ret)
ret = self.parser.parse(string, you=self.obj1, receiver=self.obj2, mapping=mapping,
ret = self.parser.parse(string, caller=self.obj1, receiver=self.obj2, mapping=mapping,
raise_errors=True)
self.assertEqual(expected_them, ret)
@ -346,10 +348,26 @@ class TestDefaultCallables(TestCase):
def test_random(self):
string = "$random(1,10)"
ret = self.parser.parse(string, raise_errors=True)
ret = int(ret)
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)
string = "$random(1.0, 3.0)"
for i in range(1000):
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):
string = "$randint(1.0, 3.0)"
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(isinstance(ret, int))
self.assertTrue(1.0 <= ret <= 3.0)
def test_nofunc(self):
self.assertEqual(
self.parser.parse("as$382ewrw w we w werw,|44943}"),

View file

@ -8,6 +8,7 @@ TODO: Not nearly all utilities are covered yet.
import os.path
import random
from parameterized import parameterized
import mock
from django.test import TestCase
from datetime import datetime
@ -385,3 +386,43 @@ class TestPercent(TestCase):
self.assertEqual(utils.percent(3, 1, 1), "0.0%")
self.assertEqual(utils.percent(3, 0, 1), "100.0%")
self.assertEqual(utils.percent(-3, 0, 1), "0.0%")
class TestSafeConvert(TestCase):
"""
Test evennia.utils.utils.safe_convert_to_types
"""
@parameterized.expand([
(('1', '2', 3, 4, '5'), {'a': 1, 'b': '2', 'c': 3},
((int, float, str, int), {'a': int, 'b': float}), # "
(1, 2.0, '3', 4, '5'), {'a': 1, 'b': 2.0, 'c': 3}),
(('1 + 2', '[1, 2, 3]', [3, 4, 5]), {'a': '3 + 4', 'b': 5},
(('py', 'py', 'py'), {'a': 'py', 'b': 'py'}),
(3, [1, 2, 3], [3, 4, 5]), {'a': 7, 'b': 5}),
])
def test_conversion(self, args, kwargs, converters, expected_args, expected_kwargs):
"""
Test the converter with different inputs
"""
result_args, result_kwargs = utils.safe_convert_to_types(
converters, *args, raise_errors=True, **kwargs)
self.assertEqual(expected_args, result_args)
self.assertEqual(expected_kwargs, result_kwargs)
def test_conversion__fail(self):
"""
Test failing conversion
"""
from evennia.utils.funcparser import ParsingError
with self.assertRaises(ValueError):
utils.safe_convert_to_types(
(int, ), *('foo', ), raise_errors=True)
with self.assertRaises(ParsingError) as err:
utils.safe_convert_to_types(
('py', {}), *('foo', ), raise_errors=True)

View file

@ -20,6 +20,8 @@ import traceback
import importlib
import importlib.util
import importlib.machinery
from ast import literal_eval
from simpleeval import simple_eval
from unicodedata import east_asian_width
from twisted.internet.task import deferLater
from twisted.internet.defer import returnValue # noqa - used as import target
@ -2390,3 +2392,104 @@ def interactive(func):
return ret
return decorator
def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
"""
Helper function to safely convert inputs to expected data types.
Args:
converters (tuple): A tuple `((converter, converter,...), {kwarg: converter, ...})` to
match a converter to each element in `*args` and `**kwargs`.
Each converter will will be called with the arg/kwarg-value as the only argument.
If there are too few converters given, the others will simply not be converter. If the
converter is given as the string 'py', it attempts to run
`safe_eval`/`literal_eval` on the input arg or kwarg value. It's possible to
skip the arg/kwarg part of the tuple, an empty tuple/dict will then be assumed.
*args: The arguments to convert with `argtypes`.
raise_errors (bool, optional): If set, raise any errors. This will
abort the conversion at that arg/kwarg. Otherwise, just skip the
conversion of the failing arg/kwarg. This will be set by the FuncParser if
this is used as a part of a FuncParser callable.
**kwargs: The kwargs to convert with `kwargtypes`
Returns:
tuple: `(args, kwargs)` in converted form.
Raises:
utils.funcparser.ParsingError: If parsing failed in the `'py'`
converter. This also makes this compatible with the FuncParser
interface.
any: Any other exception raised from other converters, if raise_errors is True.
Notes:
This function is often used to validate/convert input from untrusted sources. For
security, the "py"-converter is deliberately limited and uses `safe_eval`/`literal_eval`
which only supports simple expressions or simple containers with literals. NEVER
use the python `eval` or `exec` methods as a converter for any untrusted input! Allowing
untrusted sources to execute arbitrary python on your server is a severe security risk,
Example:
::
$funcname(1, 2, 3.0, c=[1,2,3])
def _funcname(*args, **kwargs):
args, kwargs = safe_convert_input(((int, int, float), {'c': 'py'}), *args, **kwargs)
# ...
"""
def _safe_eval(inp):
if not inp:
return ''
if not isinstance(inp, str):
# already converted
return inp
try:
return literal_eval(inp)
except Exception as err:
literal_err = f"{err.__class__.__name__}: {err}"
try:
return simple_eval(inp)
except Exception as err:
simple_err = f"{str(err.__class__.__name__)}: {err}"
pass
if raise_errors:
from evennia.utils.funcparser import ParsingError
err = (f"Errors converting '{inp}' to python:\n"
f"literal_eval raised {literal_err}\n"
f"simple_eval raised {simple_err}")
raise ParsingError(err)
# handle an incomplete/mixed set of input converters
if not converters:
return args, kwargs
arg_converters, *kwarg_converters = converters
arg_converters = make_iter(arg_converters)
kwarg_converters = kwarg_converters[0] if kwarg_converters else {}
# apply the converters
if args and arg_converters:
args = list(args)
arg_converters = make_iter(arg_converters)
for iarg, arg in enumerate(args[:len(arg_converters)]):
converter = arg_converters[iarg]
converter = _safe_eval if converter in ('py', 'python') else converter
try:
args[iarg] = converter(arg)
except Exception:
if raise_errors:
raise
args = tuple(args)
if kwarg_converters and isinstance(kwarg_converters, dict):
for key, converter in kwarg_converters.items():
converter = _safe_eval if converter in ('py', 'python') else converter
if key in {**kwargs}:
try:
kwargs[key] = converter(kwargs[key])
except Exception:
if raise_errors:
raise
return args, kwargs