Merge branch 'develop' into global-scripts-raise

This commit is contained in:
InspectorCaracal 2022-10-28 09:30:09 -06:00 committed by GitHub
commit d1981bd15b
62 changed files with 1995 additions and 818 deletions

View file

@ -61,8 +61,9 @@ Use as follows:
# create a new form from the template
form = EvForm("path/to/testform.py")
(EvForm can also take a dictionary holding
the required keys FORMCHAR, TABLECHAR and FORM)
# EvForm can also take a dictionary instead of a filepath, as long
# as the dict contains the keys FORMCHAR, TABLECHAR and FORM
# form = EvForm(form=form_dict)
# add data to each tagged form cell
form.map(cells={1: "Tom the Bouncer",

View file

@ -354,7 +354,6 @@ class FuncParser:
if curr_func:
# we are starting a nested funcdef
return_str = True
if len(callstack) > _MAX_NESTING:
# stack full - ignore this function
if raise_errors:
@ -799,7 +798,7 @@ def funcparser_callable_round(*args, **kwargs):
num, *significant = args
significant = significant[0] if significant else 0
try:
round(num, significant)
return round(num, significant)
except Exception:
if kwargs.get("raise_errors"):
raise
@ -867,22 +866,33 @@ def funcparser_callable_choice(*args, **kwargs):
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(key, flower, house)`
- `$choice([1, 2, 3, 4])`
"""
if not args:
return ""
args, _ = safe_convert_to_types(("py", {}), *args, **kwargs)
if not args[0]:
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[0])
return random.choice(args)
except Exception:
if kwargs.get("raise_errors"):
raise
@ -1153,7 +1163,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
if not targets:
if return_list:
return []
raise ParsingError(f"$search: Query '{query}' gave no matches.")
raise ParsingError(f"$search: Query '{args[0]}' gave no matches.")
if len(targets) > 1 and not return_list:
raise ParsingError(
@ -1162,7 +1172,7 @@ def funcparser_callable_search(*args, caller=None, access="control", **kwargs):
)
for target in targets:
if not target.access(caller, target, access):
if not target.access(caller, access):
raise ParsingError("$search Cannot add found entity - access failure.")
return list(targets) if return_list else targets[0]

View file

@ -384,7 +384,7 @@ class EvenniaLogFile(logfile.LogFile):
from django.conf import settings
_CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES
num_lines_to_append = _CHANNEL_LOG_NUM_TAIL_LINES
num_lines_to_append = max(1, _CHANNEL_LOG_NUM_TAIL_LINES)
def rotate(self, num_lines_to_append=None):
"""
@ -463,7 +463,7 @@ def _open_log_file(filename):
from django.conf import settings
_LOGDIR = settings.LOG_DIR
_LOG_ROTATE_SIZE = settings.CHANNEL_LOG_ROTATE_SIZE
_LOG_ROTATE_SIZE = max(1000, settings.CHANNEL_LOG_ROTATE_SIZE)
filename = os.path.join(_LOGDIR, filename)
if filename in _LOG_FILE_HANDLES:

View file

@ -41,6 +41,7 @@ __all__ = (
"search_script_tag",
"search_account_tag",
"search_channel_tag",
"search_typeclass",
)
@ -362,3 +363,35 @@ def search_channel_tag(key=None, category=None, tagtype=None, **kwargs):
# search for tag objects (not the objects they are attached to
search_tag_object = ObjectDB.objects.get_tag
# Locate Objects by Typeclass
# search_objects_by_typeclass(typeclass="", include_children=False, include_parents=False) (also search_typeclass works)
# This returns the objects of the given typeclass
def search_objects_by_typeclass(typeclass, include_children=False, include_parents=False):
"""
Searches through all objects returning those of a certain typeclass.
Args:
typeclass (str or class): A typeclass class or a python path to a typeclass.
include_children (bool, optional): Return objects with
given typeclass *and* all children inheriting from this
typeclass. Mutuall exclusive to `include_parents`.
include_parents (bool, optional): Return objects with
given typeclass *and* all parents to this typeclass.
Mutually exclusive to `include_children`.
Returns:
objects (list): The objects found with the given typeclasses.
"""
return ObjectDB.objects.typeclass_search(
typeclass=typeclass,
include_children=include_children,
include_parents=include_parents,
)
search_typeclass = search_objects_by_typeclass

View file

@ -5,6 +5,7 @@ Test the funcparser module.
"""
import time
import unittest
from ast import literal_eval
from unittest.mock import MagicMock, patch
@ -77,6 +78,10 @@ def _lsum_callable(*args, **kwargs):
return ""
def _raises_callable(*args, **kwargs):
raise RuntimeError("Test exception raised by test callable")
_test_callables = {
"foo": _test_callable,
"bar": _test_callable,
@ -89,6 +94,7 @@ _test_callables = {
"add": _add_callable,
"lit": _lit_callable,
"sum": _lsum_callable,
"raise": _raises_callable,
}
@ -102,6 +108,22 @@ class TestFuncParser(TestCase):
self.parser = funcparser.FuncParser(_test_callables)
def test_constructor_wrong_args(self):
# Given list argument doesn't contain modules or paths.
with self.assertRaises(AttributeError):
parser = funcparser.FuncParser(["foo", _test_callable])
def test_constructor_ignore_non_callables(self):
# Ignores callables that aren't actual functions.
parser = funcparser.FuncParser({"foo": 1, "bar": "baz"})
@patch("evennia.utils.funcparser.variable_from_module")
def test_constructor_raises(self, patched_variable_from_module):
# Patched variable from module returns FUNCPARSER_CALLABLES that isn't dict.
patched_variable_from_module.return_value = ["foo"]
with self.assertRaises(funcparser.ParsingError):
parser = funcparser.FuncParser("foo.module")
@parameterized.expand(
[
("Test normal string", "Test normal string"),
@ -216,13 +238,49 @@ class TestFuncParser(TestCase):
# print(f"time: {(t1-t0)*1000} ms")
self.assertEqual(expected, ret)
def test_parse_raise(self):
@parameterized.expand(
(
"Test malformed This is $dummy(a, b) and $bar(",
"Test $funcNotFound()",
)
)
def test_parse_raise_unparseable(self, unparseable):
"""
Make sure error is raised if told to do so.
"""
string = "Test malformed This is $dummy(a, b) and $bar("
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(unparseable, raise_errors=True)
@patch("evennia.utils.funcparser._MAX_NESTING", 2)
def test_parse_max_nesting(self):
"""
Make sure it is an error if the max nesting value is reached.
TODO: Does this make sense? When it sees the first function, len(callstack)
is 0. It doesn't raise until the stack length is greater than the
_MAX_NESTING value, which means you can nest 4 values with a value of
2, as demonstrated by this test.
"""
string = "$add(1, $add(1, $add(1, $toint(42))))"
ret = self.parser.parse(string)
# TODO: Does this return value actually make sense?
# It removed the spaces from the calls.
self.assertEqual("$add(1,$add(1,$add(1,$toint(42))))", ret)
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, raise_errors=True)
def test_parse_underlying_exception(self):
string = "test $add(1, 1) $raise()"
ret = self.parser.parse(string)
# TODO: Does this return value actually make sense?
# It completed the first function call.
self.assertEqual("test 2 $raise()", ret)
with self.assertRaises(RuntimeError):
self.parser.parse(string, raise_errors=True)
def test_parse_strip(self):
@ -234,6 +292,12 @@ class TestFuncParser(TestCase):
ret = self.parser.parse(string, strip=True)
self.assertEqual("Test and things", ret)
@unittest.skip("broken due to https://github.com/evennia/evennia/issues/2927")
def test_parse_whitespace_preserved(self):
string = "The answer is $add(1, x)"
ret = self.parser.parse(string)
self.assertEqual("The answer is $add(1, x)", ret)
def test_parse_escape(self):
"""
Test the parser's escape functionality.
@ -368,8 +432,7 @@ class TestDefaultCallables(TestCase):
)
def test_conjugate(self, string, expected_you, expected_them):
"""
Test callables with various input strings
Test the $conj(), $you() and $pron callables with various input strings.
"""
mapping = {"char1": self.obj1, "char2": self.obj2}
ret = self.parser.parse(
@ -381,6 +444,46 @@ class TestDefaultCallables(TestCase):
)
self.assertEqual(expected_them, ret)
def test_conjugate_missing_args(self):
string = "You $conj(smile)"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, raise_errors=True)
@parameterized.expand(
[
("male", "Char1 smiles at himself"),
("female", "Char1 smiles at herself"),
("neutral", "Char1 smiles at itself"),
("plural", "Char1 smiles at itself"),
]
)
def test_pronoun_gender(self, gender, expected):
string = "Char1 smiles at $pron(yourself)"
self.obj1.gender = gender
ret = self.parser.parse(string, caller=self.obj1, raise_errors=True)
self.assertEqual(expected, ret)
self.obj1.gender = lambda: gender
ret = self.parser.parse(string, caller=self.obj1, raise_errors=True)
self.assertEqual(expected, ret)
def test_pronoun_viewpoint(self):
string = "Char1 smiles at $pron(I)"
ret = self.parser.parse(string, caller=self.obj1, viewpoint="op", raise_errors=True)
self.assertEqual("Char1 smiles at it", ret)
def test_pronoun_capitalize(self):
string = "Char1 smiles at $pron(I)"
ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True)
self.assertEqual("Char1 smiles at It", ret)
string = "Char1 smiles at $Pron(I)"
ret = self.parser.parse(string, caller=self.obj1, capitalize=True, raise_errors=True)
self.assertEqual("Char1 smiles at It", ret)
@parameterized.expand(
[
("Test $pad(Hello, 20, c, -) there", "Test -------Hello-------- there"),
@ -396,6 +499,7 @@ class TestDefaultCallables(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 $toint(3 + 3) things", "Some 6 things"),
("Some $ljust(Hello, 30)", "Some Hello "),
("Some $rjust(Hello, 30)", "Some Hello"),
("Some $rjust(Hello, width=30)", "Some Hello"),
@ -415,6 +519,33 @@ class TestDefaultCallables(TestCase):
("There is $an(thing) here", "There is a thing here"),
("Some $eval(\"'-'*20\")Hello", "Some --------------------Hello"),
('$crop("spider\'s silk", 5)', "spide"),
("$an(apple)", "an apple"),
# These two are broken because of https://github.com/evennia/evennia/issues/2912
# ("$round(2.9) apples", "3.0 apples"),
# ("$round(2.967, 1) apples", "3.0 apples"),
# Degenerate cases
("$int2str() apples", " apples"),
("$int2str(x) apples", "x apples"),
("$int2str(1 + 1) apples", "1 + 1 apples"),
("$int2str(13) apples", "13 apples"),
("$toint([1, 2, 3]) apples", "[1, 2, 3] apples"),
("$an() foo bar", " foo bar"),
("$add(1) apple", " apple"),
("$add(1, [1, 2]) apples", " apples"),
("$round() apples", " apples"),
("$choice() apple", " apple"),
("A $pad() apple", "A apple"),
("A $pad(tasty, 13, x, -) apple", "A ----tasty---- apple"),
("A $crop() apple", "A apple"),
("A $space() apple", "A apple"),
("A $justify() apple", "A apple"),
("A $clr() apple", "A apple"),
("A $clr(red) apple", "A red apple"),
("10 $pluralize()", "10 "),
("10 $pluralize(apple, 10)", "10 apples"),
("1 $pluralize(apple)", "1 apple"),
("You $conj()", "You "),
("$pron() smiles", " smiles"),
]
)
def test_other_callables(self, string, expected):
@ -426,6 +557,9 @@ class TestDefaultCallables(TestCase):
self.assertEqual(expected, ret)
def test_random(self):
"""
Test random callable, with ranges of expected values.
"""
string = "$random(1,10)"
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
@ -436,12 +570,52 @@ class TestDefaultCallables(TestCase):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(0 <= ret <= 1)
string = "$random(2)"
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(0 <= ret <= 2)
string = "$random(1.0, 3.0)"
for i in range(100):
ret = self.parser.parse_to_any(string, raise_errors=True)
self.assertTrue(isinstance(ret, float))
self.assertTrue(1.0 <= ret <= 3.0)
string = "$random([1,2]) apples"
ret = self.parser.parse_to_any(string)
self.assertEqual(" apples", ret)
with self.assertRaises(TypeError):
ret = self.parser.parse_to_any(string, raise_errors=True)
# @unittest.skip("underlying function seems broken")
def test_choice(self):
"""
Test choice callable, where output could be either choice.
"""
string = "$choice(red, green) apple"
ret = self.parser.parse(string)
self.assertIn(ret, ("red apple", "green apple"))
string = "$choice([red, green]) apple"
ret = self.parser.parse(string)
self.assertIn(ret, ("red apple", "green apple"))
string = "$choice(['red', 'green']) apple"
ret = self.parser.parse(string)
self.assertIn(ret, ("red apple", "green apple"))
string = "$choice([1, 2])"
ret = self.parser.parse(string)
self.assertIn(ret, ("1", "2"))
ret = self.parser.parse_to_any(string)
self.assertIn(ret, (1, 2))
string = "$choice(1, 2)"
ret = self.parser.parse(string)
self.assertIn(ret, ("1", "2"))
ret = self.parser.parse_to_any(string)
self.assertIn(ret, (1, 2))
def test_randint(self):
string = "$randint(1.0, 3.0)"
ret = self.parser.parse_to_any(string, raise_errors=True)
@ -528,6 +702,7 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
"""
string = "$search(TestAccount, type=account)"
expected = self.account
self.account.locks.add("control:id(%s)" % self.char1.dbref)
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
self.assertEqual(expected, ret)
@ -539,6 +714,7 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
"""
string = "$search(Script, type=script)"
expected = self.script
self.script.locks.add("control:id(%s)" % self.char1.dbref)
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
self.assertEqual(expected, ret)
@ -553,3 +729,86 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
self.assertEqual(expected, ret)
def test_search_tag(self):
"""
Test searching for a tag
"""
self.char1.tags.add("foo")
string = "This is $search(foo, type=tag)"
expected = "This is %s" % str(self.char1)
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
self.assertEqual(expected, ret)
def test_search_not_found(self):
string = "$search(foo)"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=False)
self.assertEqual("$search(foo)", ret)
ret = self.parser.parse_to_any(
string, caller=self.char1, return_list=True, raise_errors=False
)
self.assertEqual([], ret)
def test_search_multiple_results_no_list(self):
"""
Test exception when search returns multiple results but list is not requested
"""
string = "$search(BaseObject)"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
def test_search_no_access(self):
string = "Go to $search(Room)"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, caller=self.char2, return_list=True, raise_errors=True)
def test_search_no_caller(self):
string = "$search(Char)"
with self.assertRaises(funcparser.ParsingError):
self.parser.parse(string, caller=None, raise_errors=True)
def test_search_no_args(self):
string = "$search()"
ret = self.parser.parse(string, caller=self.char1, return_list=False, raise_errors=True)
self.assertEqual("None", ret)
ret = self.parser.parse(string, caller=self.char1, return_list=True, raise_errors=True)
self.assertEqual("[]", ret)
def test_search_nested__issue2902(self):
"""
Search for objects by-tag, check that the result is a valid object
"""
# we
parser = funcparser.FuncParser(
{**funcparser.SEARCHING_CALLABLES, **funcparser.FUNCPARSER_CALLABLES}
)
# set up search targets
self.obj1.tags.add("beach", category="zone")
self.obj2.tags.add("beach", category="zone")
# first a plain search
string = "$objlist(beach,category=zone,type=tag)"
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
self.assertEqual(ret, [self.obj1, self.obj2])
# get random result from the possible matches
string = "$choice($objlist(beach,category=zone,type=tag))"
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
self.assertIn(ret, [self.obj1, self.obj2])
# test wrapping in $obj(), should just pass object through
string = "$obj($choice($objlist(beach,category=zone,type=tag)))"
ret = parser.parse_to_any(string, caller=self.char1, raise_errors=True)
self.assertIn(ret, [self.obj1, self.obj2])

View file

@ -1,6 +1,13 @@
from evennia import DefaultObject, DefaultRoom
from evennia.objects.models import ObjectDB
from evennia.scripts.scripts import DefaultScript
from evennia.utils.search import (
search_script,
search_script_attribute,
search_script_tag,
search_typeclass,
)
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.search import search_script_attribute, search_script_tag, search_script
class TestSearch(EvenniaTest):
@ -61,3 +68,15 @@ class TestSearch(EvenniaTest):
script, errors = DefaultScript.create("a-script")
found = search_script("wrong_key")
self.assertEqual(len(found), 0, errors)
def test_search_typeclass(self):
"""Check that an object can be found by typeclass"""
DefaultObject.create("test_obj")
found = search_typeclass("evennia.objects.objects.DefaultObject")
self.assertEqual(len(found), 1)
def test_search_wrong_typeclass(self):
"""Check that an object cannot be found by wrong typeclass"""
DefaultObject.create("test_obj_2")
with self.assertRaises(ImportError):
search_typeclass("not.a.typeclass")

View file

@ -66,8 +66,12 @@ class TestListToString(TestCase):
[1,2,3] -> '1, 2, 3'
with sep==';' and endsep==';':
[1,2,3] -> '1; 2; 3'
with sep=='or':
[1,2,3] -> '1 or 2, and 3'
with endsep=='and':
[1,2,3] -> '1, 2 and 3'
with endsep=='; and':
[1,2,3] -> '1, 2; and 3'
with endsep=='':
[1,2,3] -> '1, 2 3'
with addquote and endsep="and"
@ -80,6 +84,8 @@ class TestListToString(TestCase):
self.assertEqual("1, 2 and 3", utils.list_to_string([1, 2, 3], endsep="and"))
self.assertEqual("1, 2 3", utils.list_to_string([1, 2, 3], endsep=""))
self.assertEqual("1; 2; 3", utils.list_to_string([1, 2, 3], sep=";", endsep=";"))
self.assertEqual("1 or 2, and 3", utils.list_to_string([1, 2, 3], sep="or"))
self.assertEqual("1, 2; and 3", utils.list_to_string([1, 2, 3], endsep="; and"))
self.assertEqual(
'"1", "2", "3"', utils.list_to_string([1, 2, 3], endsep=",", addquote=True)
)
@ -696,3 +702,36 @@ class TestDelay(BaseEvenniaTest):
timedelay
) # Clock must advance to trigger, even if past timedelay
self.assertEqual(self.char1.ndb.dummy_var, "dummy_func ran")
class TestIntConversions(TestCase):
def test_int2str(self):
self.assertEqual("three", utils.int2str(3))
# special adjective conversion
self.assertEqual("3rd", utils.int2str(3, adjective=True))
# generic adjective conversion
self.assertEqual("5th", utils.int2str(5, adjective=True))
# No mapping return int as str
self.assertEqual("15", utils.int2str(15))
def test_str2int(self):
# simple conversions
self.assertEqual(5, utils.str2int("5"))
# basic mapped numbers
self.assertEqual(3, utils.str2int("three"))
self.assertEqual(20, utils.str2int("twenty"))
# multi-place numbers
self.assertEqual(2345, utils.str2int("two thousand, three hundred and forty-five"))
# ordinal numbers
self.assertEqual(1, utils.str2int("1st"))
self.assertEqual(1, utils.str2int("first"))
self.assertEqual(4, utils.str2int("fourth"))
# ordinal sound-change conversions
self.assertEqual(5, utils.str2int("fifth"))
self.assertEqual(20, utils.str2int("twentieth"))
with self.assertRaises(ValueError):
utils.str2int("not a number")

View file

@ -24,6 +24,7 @@ from ast import literal_eval
from collections import OrderedDict, defaultdict
from inspect import getmembers, getmodule, getmro, ismodule, trace
from os.path import join as osjoin
from string import punctuation
from unicodedata import east_asian_width
from django.apps import apps
@ -409,12 +410,17 @@ def iter_to_str(iterable, sep=",", endsep=", and", addquote=False):
else:
iterable = tuple(str(val) for val in iterable)
if endsep.startswith(sep):
# oxford comma alternative
endsep = endsep[1:] if len_iter < 3 else endsep
elif endsep:
# normal space-separated end separator
endsep = " " + str(endsep).strip()
if endsep:
if endsep.startswith(sep):
# oxford comma alternative
endsep = endsep[1:] if len_iter < 3 else endsep
elif endsep[0] not in punctuation:
# add a leading space if endsep is a word
endsep = " " + str(endsep).strip()
# also add a leading space if separator is a word
if sep not in punctuation:
sep = " " + sep
if len_iter == 1:
return str(iterable[0])
@ -2281,14 +2287,17 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
)
for num, result in enumerate(matches):
# we need to consider Commands, where .aliases is a list
aliases = result.aliases.all() if hasattr(result.aliases, "all") else result.aliases
# remove any pluralization aliases
aliases = [
alias
for alias in aliases
if hasattr(alias, "category") and alias.category not in ("plural_key",)
]
# we need to consider that result could be a Command, where .aliases
# is a list of strings
if hasattr(result.aliases, "all"):
# result is a typeclassed entity where `.aliases` is an AliasHandler.
aliases = result.aliases.all(return_objs=True)
# remove pluralization aliases
aliases = [alias for alias in aliases if alias.category not in ("plural_key",)]
else:
# result is likely a Command, where `.aliases` is a list of strings.
aliases = result.aliases
error += _MULTIMATCH_TEMPLATE.format(
number=num + 1,
name=result.get_display_name(caller)
@ -2563,6 +2572,14 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
# ...
"""
container_end_char = {"(": ")", "[": "]", "{": "}"} # tuples, lists, sets
def _manual_parse_containers(inp):
startchar = inp[0]
endchar = inp[-1]
if endchar != container_end_char.get(startchar):
return
return [str(part).strip() for part in inp[1:-1].split(",")]
def _safe_eval(inp):
if not inp:
@ -2570,16 +2587,21 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
if not isinstance(inp, str):
# already converted
return inp
try:
return literal_eval(inp)
try:
return literal_eval(inp)
except ValueError:
parts = _manual_parse_containers(inp)
if not parts:
raise
return parts
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
@ -2590,6 +2612,9 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
f"simple_eval raised {simple_err}"
)
raise ParsingError(err)
else:
# fallback - convert to str
return str(inp)
# handle an incomplete/mixed set of input converters
if not converters:
@ -2755,3 +2780,110 @@ def int2str(number, adjective=False):
if adjective:
return _INT2STR_MAP_ADJ.get(number, f"{number}th")
return _INT2STR_MAP_NOUN.get(number, str(number))
_STR2INT_MAP = {
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
"six": 6,
"seven": 7,
"eight": 8,
"nine": 9,
"ten": 10,
"eleven": 11,
"twelve": 12,
"thirteen": 13,
"fourteen": 14,
"fifteen": 15,
"sixteen": 16,
"seventeen": 17,
"eighteen": 18,
"nineteen": 19,
"twenty": 20,
"thirty": 30,
"forty": 40,
"fifty": 50,
"sixty": 60,
"seventy": 70,
"eighty": 80,
"ninety": 90,
"hundred": 100,
"thousand": 1000,
}
_STR2INT_ADJS = {
"first": 1,
"second": 2,
"third": 3,
}
def str2int(number):
"""
Converts a string to an integer.
Args:
number (str): The string to convert. It can be a digit such as "1", or a number word such as "one".
Returns:
int: The string represented as an integer.
"""
number = str(number)
original_input = number
try:
# it's a digit already
return int(number)
except:
# if it's an ordinal number such as "1st", it'll convert to int with the last two characters chopped off
try:
return int(number[:-2])
except:
pass
# convert sound changes for generic ordinal numbers
if number[-2:] == "th":
# remove "th"
number = number[:-2]
if number[-1] == "f":
# e.g. twelfth, fifth
number = number[:-1] + "ve"
elif number[-2:] == "ie":
# e.g. twentieth, fortieth
number = number[:-2] + "y"
# custom case for ninth
elif number[-3:] == "nin":
number += "e"
if i := _STR2INT_MAP.get(number):
# it's a single number, return it
return i
# remove optional "and"s
number = number.replace(" and ", " ")
# split number words by spaces, hyphens and commas, to accommodate multiple styles
numbers = [word.lower() for word in re.split(r"[-\s\,]", number) if word]
sums = []
for word in numbers:
# check if it's a known number-word
if i := _STR2INT_MAP.get(word):
if not len(sums):
# initialize the list with the current value
sums = [i]
else:
# if the previous number was smaller, it's a multiplier
# e.g. the "two" in "two hundred"
if sums[-1] < i:
sums[-1] = sums[-1] * i
# otherwise, it's added on, like the "five" in "twenty five"
else:
sums.append(i)
elif i := _STR2INT_ADJS.get(word):
# it's a special adj word; ordinal case will never be a multiplier
sums.append(i)
else:
# invalid number-word, raise ValueError
raise ValueError(f"String {original_input} cannot be converted to int.")
return sum(sums)

View file

@ -2,7 +2,7 @@
English pronoun mapping between 1st/2nd person and 3rd person perspective (and vice-versa).
This file is released under the Evennia regular BSD License.
(Griatch 2021)
(Griatch 2021) - revised by InspectorCaracal 2022
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. This mapping maps between 1st/2nd case and
@ -21,227 +21,266 @@ viewpoint/pronouns Subject Object Possessive Possessive Reflexive
3rd person male he him his his himself
3rd person female she her her hers herself
3rd person neutral it it its theirs* itself
3rd person neutral it it its its itself
3rd person plural they them their theirs themselves
==================== ======= ======== ========== ========== ===========
> `*`) Not formally used, we use `theirs` here as a filler.
"""
from evennia.utils.utils import copy_word_case
from evennia.utils.utils import copy_word_case, is_iter
DEFAULT_PRONOUN_TYPE = "object_pronoun"
DEFAULT_PRONOUN_TYPE = "subject pronoun"
DEFAULT_VIEWPOINT = "2nd person"
DEFAULT_GENDER = "neutral"
PRONOUN_TYPES = [
"subject pronoun",
"object pronoun",
"possessive adjective",
"possessive pronoun",
"reflexive pronoun",
]
VIEWPOINTS = ["1st person", "2nd person", "3rd person"]
GENDERS = ["male", "female", "neutral", "plural"]
PRONOUN_MAPPING = {
# 1st/2nd person -> 3rd person mappings
"I": {"subject pronoun": {"3rd person": {"male": "he", "female": "she", "neutral": "it"}}},
"me": {"object pronoun": {"3rd person": {"male": "him", "female": "her", "neutral": "it"}}},
"my": {
"possessive adjective": {"3rd person": {"male": "his", "female": "her", "neutral": "its"}}
},
"mine": {
"possessive pronoun": {
"3rd person": {
"male": "his",
"female": "hers",
"neutral": "theirs", # colloqial,
}
}
},
"myself": {
"reflexive_pronoun": {
"3rd person": {
"male": "himself",
"female": "herself",
"neutral": "itself",
"plural": "themselves",
}
}
},
"you": {
"1st person": {
"subject pronoun": {
"3rd person": {
"male": "he",
"female": "she",
"neutral": "it",
"plural": "they",
}
"neutral": "I",
"plural": "we",
},
"object pronoun": {
"3rd person": {
"male": "him",
"female": "her",
"neutral": "it",
"plural": "them",
}
"neutral": "me",
"plural": "us",
},
},
"your": {
"possessive adjective": {
"3rd person": {
"male": "his",
"female": "her",
"neutral": "its",
"plural": "their",
}
}
},
"yours": {
"possessive pronoun": {
"3rd person": {
"male": "his",
"female": "hers",
"neutral": "theirs", # colloqial
"plural": "theirs",
}
}
},
"yourself": {
"reflexive_pronoun": {
"3rd person": {
"male": "himself",
"female": "herself",
"neutral": "itself",
}
}
},
"we": {"subject pronoun": {"3rd person": {"plural": "they"}}},
"us": {"object pronoun": {"3rd person": {"plural": "them"}}},
"our": {"possessive adjective": {"3rd person": {"plural": "their"}}},
"ours": {"possessive pronoun": {"3rd person": {"plural": "theirs"}}},
"ourselves": {"reflexive pronoun": {"3rd person": {"plural": "themselves"}}},
"ours": {"possessive pronoun": {"3rd person": {"plural": "theirs"}}},
"ourselves": {"reflexive pronoun": {"3rd person": {"plural": "themselves"}}},
"yourselves": {"reflexive_pronoun": {"3rd person": {"plural": "themselves"}}},
# 3rd person to 1st/second person mappings
"he": {
"subject pronoun": {
"1st person": {"neutral": "I", "plural": "we"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
}
},
"him": {
"object pronoun": {
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
}
},
"his": {
"possessive adjective": {
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
"neutral": "my",
"plural": "our",
},
"possessive pronoun": {
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
"neutral": "mine",
"plural": "ours",
},
},
"himself": {
"reflexive pronoun": {
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
},
},
"she": {
"subject pronoun": {
"1st person": {"neutral": "I", "plural": "you"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "we"}, # pluralis majestatis
"neutral": "myself",
"plural": "ourselves"
}
},
"her": {
"2nd person": {
"subject pronoun": {
"neutral": "you",
},
"object pronoun": {
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
"neutral": "you",
},
"possessive adjective": {
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
"neutral": "your",
},
},
"hers": {
"possessive pronoun": {
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
"neutral": "yours",
},
"reflexive pronoun": {
"neutral": "yourself",
"plural": "yourselves",
}
},
"herself": {
"reflexive pronoun": {
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
"2nd person": {"neutral": "yourself", "plural": "yourselves"}, # pluralis majestatis
},
},
"it": {
"3rd person": {
"subject pronoun": {
"1st person": {"neutral": "I", "plural": "we"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
"male": "he",
"female": "she",
"neutral": "it",
"plural": "they"
},
"object pronoun": {
"1st person": {"neutral": "me", "plural": "us"}, # pluralis majestatis
"2nd person": {"neutral": "you", "plural": "you"}, # pluralis majestatis
"male": "him",
"female": "her",
"neutral": "it",
"plural": "them"
},
},
"its": {
"possessive adjective": {
"1st person": {"neutral": "my", "plural": "our"}, # pluralis majestatis
"2nd person": {"neutral": "your", "plural": "your"}, # pluralis majestatis
}
},
"theirs": {
"male": "his",
"female": "her",
"neutral": "its",
"plural": "their"
},
"possessive pronoun": {
"1st person": {"neutral": "mine", "plural": "ours"}, # pluralis majestatis
"2nd person": {"neutral": "yours", "plural": "yours"}, # pluralis majestatis
}
},
"itself": {
"reflexive pronoun": {
"1st person": {"neutral": "myself", "plural": "ourselves"}, # pluralis majestatis
"2nd person": {"neutral": "yourself", "plural": "yourselves"}, # pluralis majestatis
"male": "his",
"female": "hers",
"neutral": "its",
"plural": "theirs",
},
},
"they": {
"subject pronoun": {
"1st person": {
"plural": "we",
},
"2nd person": {
"plural": "you",
},
}
},
"them": {
"object pronoun": {
"1st person": {
"plural": "us",
},
"2nd person": {
"plural": "you",
},
}
},
"their": {
"possessive adjective": {
"1st person": {
"plural": "our",
},
"2nd person": {
"plural": "your",
},
}
},
"themselves": {
"reflexive pronoun": {
"1st person": {
"plural": "ourselves",
},
"2nd person": {
"plural": "yourselves",
},
}
},
"male": "himself",
"female": "herself",
"neutral": "itself",
"plural": "themselves",
},
}
}
PRONOUN_TABLE = {
"I": (
"1st person",
("neutral", "male", "female"),
"subject pronoun"
),
"me": (
"1st person",
("neutral", "male", "female"),
"object pronoun"
),
"my": (
"1st person",
("neutral", "male", "female"),
"possessive adjective"
),
"mine": (
"1st person",
("neutral", "male", "female"),
"possessive pronoun"
),
"myself": (
"1st person",
("neutral", "male", "female"),
"reflexive pronoun"
),
"we": (
"1st person",
"plural",
"subject pronoun"
),
"us": (
"1st person",
"plural",
"object pronoun"
),
"our": (
"1st person",
"plural",
"possessive adjective"
),
"ours": (
"1st person",
"plural",
"possessive pronoun"
),
"ourselves": (
"1st person",
"plural",
"reflexive pronoun"
),
"you": (
"2nd person",
("neutral", "male", "female", "plural"),
("subject pronoun", "object pronoun")
),
"your": (
"2nd person",
("neutral", "male", "female", "plural"),
"possessive adjective"
),
"yours": (
"2nd person",
("neutral", "male", "female", "plural"),
"possessive pronoun"
),
"yourself": (
"2nd person",
("neutral", "male", "female"),
"reflexive pronoun"
),
"yourselves": (
"2nd person",
"plural",
"reflexive pronoun"
),
"he": (
"3rd person",
"male",
"subject pronoun"
),
"him": (
"3rd person",
"male",
"object pronoun"
),
"his":(
"3rd person",
"male",
("possessive pronoun","possessive adjective"),
),
"himself": (
"3rd person",
"male",
"reflexive pronoun"
),
"she": (
"3rd person",
"female",
"subject pronoun"
),
"her": (
"3rd person",
"female",
("object pronoun", "possessive adjective"),
),
"hers": (
"3rd person",
"female",
"possessive pronoun"
),
"herself": (
"3rd person",
"female",
"reflexive pronoun"
),
"it": (
"3rd person",
"neutral",
("subject pronoun", "object pronoun"),
),
"its": (
"3rd person",
"neutral",
("possessive pronoun", "possessive adjective"),
),
"itself": (
"3rd person",
"neutral",
"reflexive pronoun"
),
"they": (
"3rd person",
"plural",
"subject pronoun"
),
"them": (
"3rd person",
"plural",
"object pronoun"
),
"their": (
"3rd person",
"plural",
"possessive adjective"
),
"theirs": (
"3rd person",
"plural",
"possessive pronoun"
),
"themselves": (
"3rd person",
"plural",
"reflexive pronoun"
),
}
# define the default viewpoint conversions
VIEWPOINT_CONVERSION = {
"1st person": "3rd person",
"2nd person": "3rd person",
"3rd person": ("1st person", "2nd person"),
}
ALIASES = {
"m": "male",
@ -263,19 +302,9 @@ ALIASES = {
"pp": "possessive pronoun",
}
PRONOUN_TYPES = [
"subject pronoun",
"object pronoun",
"possessive adjective",
"possessive pronoun",
"reflexive pronoun",
]
VIEWPOINTS = ["1st person", "2nd person", "3rd person"]
GENDERS = ["male", "female", "neutral", "plural"] # including plural as a gender for simplicity
def pronoun_to_viewpoints(
pronoun, options=None, pronoun_type="object_pronoun", gender="neutral", viewpoint="2nd person"
pronoun, options=None, pronoun_type=DEFAULT_PRONOUN_TYPE, gender=DEFAULT_GENDER, viewpoint=DEFAULT_VIEWPOINT
):
"""
Access function for determining the forms of a pronount from different viewpoints.
@ -292,7 +321,7 @@ def pronoun_to_viewpoints(
- `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)
- `possessive pronoun`/`pronoun`/`pp` (mine, yours, his, theirs)
gender (str, optional): Specific gender to use (plural counts a gender for this purpose).
A gender specified in `options` takes precedence. Values and aliases are:
@ -323,18 +352,20 @@ def pronoun_to_viewpoints(
pronoun_lower = "I" if pronoun == "I" else pronoun.lower()
if pronoun_lower not in PRONOUN_MAPPING:
if pronoun_lower not in PRONOUN_TABLE:
return pronoun
# differentiators
# get the default data for the input pronoun
source_viewpoint, source_gender, source_type = PRONOUN_TABLE[pronoun_lower]
# differentiators
if pronoun_type not in PRONOUN_TYPES:
pronoun_type = DEFAULT_PRONOUN_TYPE
if viewpoint not in VIEWPOINTS:
viewpoint = DEFAULT_VIEWPOINT
if gender not in GENDERS:
gender = DEFAULT_GENDER
if options:
# option string/list will override the kwargs differentiators given
if isinstance(options, str):
@ -350,44 +381,35 @@ def pronoun_to_viewpoints(
elif opt in GENDERS:
gender = opt
# step down into the mapping, using differentiators as needed
pronoun_types = PRONOUN_MAPPING[pronoun_lower]
# this has one or more pronoun-types
if len(pronoun_types) == 1:
pronoun_type, viewpoints = next(iter(pronoun_types.items()))
elif pronoun_type in pronoun_types:
viewpoints = pronoun_types[pronoun_type]
elif DEFAULT_PRONOUN_TYPE in pronoun_types:
pronoun_type = DEFAULT_PRONOUN_TYPE
viewpoints = pronoun_types[pronoun_type]
# check if pronoun maps to multiple options and differentiate
# but don't allow invalid differentiators
if is_iter(source_type):
pronoun_type = pronoun_type if pronoun_type in source_type else source_type[0]
else:
# not enough info - grab the first of the mappings
pronoun_type, viewpoints = next(iter(pronoun_types.items()))
# we have one or more viewpoints at this point
if len(viewpoints) == 1:
viewpoint, genders = next(iter(viewpoints.items()))
elif viewpoint in viewpoints:
genders = viewpoints[viewpoint]
elif DEFAULT_VIEWPOINT in viewpoints:
viewpoint = DEFAULT_VIEWPOINT
genders = viewpoints[viewpoint]
pronoun_type = source_type
target_viewpoint = VIEWPOINT_CONVERSION[source_viewpoint]
if is_iter(target_viewpoint):
viewpoint = viewpoint if viewpoint in target_viewpoint else target_viewpoint[0]
else:
# not enough info - grab first of mappings
viewpoint, genders = next(iter(viewpoints.items()))
viewpoint = target_viewpoint
# we have one or more possible genders (including plural forms)
if len(genders) == 1:
gender, mapped_pronoun = next(iter(genders.items()))
elif gender in genders:
mapped_pronoun = genders[gender]
elif DEFAULT_GENDER in genders:
gender = DEFAULT_GENDER
mapped_pronoun = genders[gender]
# special handling for the royal "we"
if is_iter(source_gender):
gender_opts = list(source_gender)
else:
# not enough info - grab first mapping
gender, mapped_pronoun = next(iter(genders.items()))
gender_opts = [source_gender]
if viewpoint == "1st person":
# make sure plural is always an option when converting to 1st person
# it doesn't matter if it's in the list twice, so don't bother checking
gender_opts.append("plural")
# if the gender is still not in the extended options, fall back to source pronoun's default
gender = gender if gender in gender_opts else gender_opts[0]
# step down into the mapping
viewpoint_map = PRONOUN_MAPPING[viewpoint]
pronouns = viewpoint_map.get(pronoun_type, viewpoint_map[DEFAULT_PRONOUN_TYPE])
mapped_pronoun = pronouns.get(gender, pronouns[DEFAULT_GENDER])
# keep the same capitalization as the original
if pronoun != "I":
# don't remap I, since this is always capitalized.
@ -396,10 +418,10 @@ def pronoun_to_viewpoints(
mapped_pronoun = mapped_pronoun.upper()
if viewpoint == "3rd person":
# the remapped viewpoing is in 3rd person, meaning the ingoing viewpoing
# the desired viewpoint is 3rd person, meaning the incoming viewpoint
# must have been 1st or 2nd person.
return pronoun, mapped_pronoun
else:
# the remapped viewpoint is 1st or 2nd person, so ingoing must have been
# the desired viewpoint is 1st or 2nd person, so incoming must have been
# in 3rd person form.
return mapped_pronoun, pronoun

View file

@ -279,7 +279,7 @@ class TestPronounMapping(TestCase):
("you", "m", "you", "he"),
("you", "f op", "you", "her"),
("I", "", "I", "it"),
("I", "p", "I", "it"), # plural is invalid
("I", "p", "I", "it"), # plural is invalid
("I", "m", "I", "he"),
("Me", "n", "Me", "It"),
("your", "p", "your", "their"),
@ -295,7 +295,6 @@ class TestPronounMapping(TestCase):
("her", "p", "you", "her"),
("her", "pa", "your", "her"),
("their", "pa", "your", "their"),
("their", "pa", "your", "their"),
("itself", "", "yourself", "itself"),
("themselves", "", "yourselves", "themselves"),
("herself", "", "yourself", "herself"),
@ -311,6 +310,5 @@ class TestPronounMapping(TestCase):
received_1st_or_2nd_person, received_3rd_person = pronouns.pronoun_to_viewpoints(
pronoun, options
)
self.assertEqual(expected_1st_or_2nd_person, received_1st_or_2nd_person)
self.assertEqual(expected_3rd_person, received_3rd_person)