Fix $choice funcparser; improve safe_eval parsing of containers. Resolve #2910.
This commit is contained in:
parent
4fb5268acd
commit
0f70f51724
3 changed files with 98 additions and 33 deletions
|
|
@ -866,22 +866,33 @@ def funcparser_callable_choice(*args, **kwargs):
|
||||||
Args:
|
Args:
|
||||||
listing (list): A list of items to randomly choose between.
|
listing (list): A list of items to randomly choose between.
|
||||||
This will be converted from a string to a real list.
|
This will be converted from a string to a real list.
|
||||||
|
*args: If multiple args are given, will pick one randomly from them.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
any: The randomly chosen element.
|
any: The randomly chosen element.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
- `$choice([key, flower, house])`
|
- `$choice(key, flower, house)`
|
||||||
- `$choice([1, 2, 3, 4])`
|
- `$choice([1, 2, 3, 4])`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not args:
|
if not args:
|
||||||
return ""
|
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 ""
|
return ""
|
||||||
try:
|
try:
|
||||||
return random.choice(args[0])
|
return random.choice(args)
|
||||||
except Exception:
|
except Exception:
|
||||||
if kwargs.get("raise_errors"):
|
if kwargs.get("raise_errors"):
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
|
|
@ -587,19 +587,35 @@ class TestDefaultCallables(TestCase):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||||
|
|
||||||
@unittest.skip("underlying function seems broken")
|
# @unittest.skip("underlying function seems broken")
|
||||||
def test_choice(self):
|
def test_choice(self):
|
||||||
"""
|
"""
|
||||||
Test choice callable, where output could be either choice.
|
Test choice callable, where output could be either choice.
|
||||||
"""
|
"""
|
||||||
string = "$choice(red, green) apple"
|
string = "$choice(red, green) apple"
|
||||||
ret = self.parser.parse(string, raise_errors=True)
|
ret = self.parser.parse(string)
|
||||||
self.assertIn(ret, ("red apple", "green apple"))
|
self.assertIn(ret, ("red apple", "green apple"))
|
||||||
|
|
||||||
string = "$choice([red, green]) apple"
|
string = "$choice([red, green]) apple"
|
||||||
ret = self.parser.parse(string, raise_errors=True)
|
ret = self.parser.parse(string)
|
||||||
self.assertIn(ret, ("red apple", "green apple"))
|
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):
|
def test_randint(self):
|
||||||
string = "$randint(1.0, 3.0)"
|
string = "$randint(1.0, 3.0)"
|
||||||
ret = self.parser.parse_to_any(string, raise_errors=True)
|
ret = self.parser.parse_to_any(string, raise_errors=True)
|
||||||
|
|
@ -726,7 +742,6 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
||||||
ret = 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=True)
|
||||||
self.assertEqual(expected, ret)
|
self.assertEqual(expected, ret)
|
||||||
|
|
||||||
@unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916")
|
|
||||||
def test_search_not_found(self):
|
def test_search_not_found(self):
|
||||||
string = "$search(foo)"
|
string = "$search(foo)"
|
||||||
with self.assertRaises(funcparser.ParsingError):
|
with self.assertRaises(funcparser.ParsingError):
|
||||||
|
|
@ -735,10 +750,11 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
||||||
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=False)
|
ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=False)
|
||||||
self.assertEqual("$search(foo)", ret)
|
self.assertEqual("$search(foo)", ret)
|
||||||
|
|
||||||
ret = self.parser.parse(string, caller=self.char1, return_list=True, raise_errors=False)
|
ret = self.parser.parse_to_any(
|
||||||
|
string, caller=self.char1, return_list=True, raise_errors=False
|
||||||
|
)
|
||||||
self.assertEqual([], ret)
|
self.assertEqual([], ret)
|
||||||
|
|
||||||
@unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916")
|
|
||||||
def test_search_multiple_results_no_list(self):
|
def test_search_multiple_results_no_list(self):
|
||||||
"""
|
"""
|
||||||
Test exception when search returns multiple results but list is not requested
|
Test exception when search returns multiple results but list is not requested
|
||||||
|
|
@ -747,7 +763,6 @@ class TestCallableSearch(test_resources.BaseEvenniaTest):
|
||||||
with self.assertRaises(funcparser.ParsingError):
|
with self.assertRaises(funcparser.ParsingError):
|
||||||
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True)
|
||||||
|
|
||||||
@unittest.skip("broken, see https://github.com/evennia/evennia/issues/2917")
|
|
||||||
def test_search_no_access(self):
|
def test_search_no_access(self):
|
||||||
string = "Go to $search(Room)"
|
string = "Go to $search(Room)"
|
||||||
with self.assertRaises(funcparser.ParsingError):
|
with self.assertRaises(funcparser.ParsingError):
|
||||||
|
|
|
||||||
|
|
@ -2563,6 +2563,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):
|
def _safe_eval(inp):
|
||||||
if not inp:
|
if not inp:
|
||||||
|
|
@ -2570,16 +2578,21 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
||||||
if not isinstance(inp, str):
|
if not isinstance(inp, str):
|
||||||
# already converted
|
# already converted
|
||||||
return inp
|
return inp
|
||||||
|
|
||||||
try:
|
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:
|
except Exception as err:
|
||||||
literal_err = f"{err.__class__.__name__}: {err}"
|
literal_err = f"{err.__class__.__name__}: {err}"
|
||||||
try:
|
try:
|
||||||
return simple_eval(inp)
|
return simple_eval(inp)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
simple_err = f"{str(err.__class__.__name__)}: {err}"
|
simple_err = f"{str(err.__class__.__name__)}: {err}"
|
||||||
pass
|
|
||||||
|
|
||||||
if raise_errors:
|
if raise_errors:
|
||||||
from evennia.utils.funcparser import ParsingError
|
from evennia.utils.funcparser import ParsingError
|
||||||
|
|
@ -2590,6 +2603,9 @@ def safe_convert_to_types(converters, *args, raise_errors=True, **kwargs):
|
||||||
f"simple_eval raised {simple_err}"
|
f"simple_eval raised {simple_err}"
|
||||||
)
|
)
|
||||||
raise ParsingError(err)
|
raise ParsingError(err)
|
||||||
|
else:
|
||||||
|
# fallback - convert to str
|
||||||
|
return str(inp)
|
||||||
|
|
||||||
# handle an incomplete/mixed set of input converters
|
# handle an incomplete/mixed set of input converters
|
||||||
if not converters:
|
if not converters:
|
||||||
|
|
@ -2756,28 +2772,52 @@ def int2str(number, adjective=False):
|
||||||
return _INT2STR_MAP_ADJ.get(number, f"{number}th")
|
return _INT2STR_MAP_ADJ.get(number, f"{number}th")
|
||||||
return _INT2STR_MAP_NOUN.get(number, str(number))
|
return _INT2STR_MAP_NOUN.get(number, str(number))
|
||||||
|
|
||||||
|
|
||||||
_STR2INT_MAP = {
|
_STR2INT_MAP = {
|
||||||
"one": 1, "two": 2, "three": 3,
|
"one": 1,
|
||||||
"four": 4, "five": 5, "six": 6,
|
"two": 2,
|
||||||
"seven": 7, "eight": 8, "nine": 9,
|
"three": 3,
|
||||||
"ten": 10, "eleven": 11, "twelve": 12,
|
"four": 4,
|
||||||
"thirteen": 13, "fourteen": 14, "fifteen": 15,
|
"five": 5,
|
||||||
"sixteen": 16, "seventeen": 17, "eighteen": 18,
|
"six": 6,
|
||||||
"nineteen": 19, "twenty": 20, "thirty": 30,
|
"seven": 7,
|
||||||
"forty": 40, "fifty": 50, "sixty": 60,
|
"eight": 8,
|
||||||
"seventy": 70, "eighty": 80, "ninety": 90,
|
"nine": 9,
|
||||||
"hundred": 100, "thousand": 1000,
|
"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 = {
|
_STR2INT_ADJS = {
|
||||||
"first": 1, "second": 2, "third": 3,
|
"first": 1,
|
||||||
|
"second": 2,
|
||||||
|
"third": 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def str2int(number):
|
def str2int(number):
|
||||||
"""
|
"""
|
||||||
Converts a string to an integer.
|
Converts a string to an integer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
number (str): The string to convert. It can be a digit such as "1", or a number word such as "one".
|
number (str): The string to convert. It can be a digit such as "1", or a number word such as "one".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: The string represented as an integer.
|
int: The string represented as an integer.
|
||||||
"""
|
"""
|
||||||
|
|
@ -2792,7 +2832,7 @@ def str2int(number):
|
||||||
return int(number[:-2])
|
return int(number[:-2])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# convert sound changes for generic ordinal numbers
|
# convert sound changes for generic ordinal numbers
|
||||||
if number[-2:] == "th":
|
if number[-2:] == "th":
|
||||||
# remove "th"
|
# remove "th"
|
||||||
|
|
@ -2812,10 +2852,10 @@ def str2int(number):
|
||||||
return i
|
return i
|
||||||
|
|
||||||
# remove optional "and"s
|
# remove optional "and"s
|
||||||
number = number.replace(" and "," ")
|
number = number.replace(" and ", " ")
|
||||||
|
|
||||||
# split number words by spaces, hyphens and commas, to accommodate multiple styles
|
# 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 ]
|
numbers = [word.lower() for word in re.split(r"[-\s\,]", number) if word]
|
||||||
sums = []
|
sums = []
|
||||||
for word in numbers:
|
for word in numbers:
|
||||||
# check if it's a known number-word
|
# check if it's a known number-word
|
||||||
|
|
@ -2827,7 +2867,7 @@ def str2int(number):
|
||||||
# if the previous number was smaller, it's a multiplier
|
# if the previous number was smaller, it's a multiplier
|
||||||
# e.g. the "two" in "two hundred"
|
# e.g. the "two" in "two hundred"
|
||||||
if sums[-1] < i:
|
if sums[-1] < i:
|
||||||
sums[-1] = sums[-1]*i
|
sums[-1] = sums[-1] * i
|
||||||
# otherwise, it's added on, like the "five" in "twenty five"
|
# otherwise, it's added on, like the "five" in "twenty five"
|
||||||
else:
|
else:
|
||||||
sums.append(i)
|
sums.append(i)
|
||||||
|
|
@ -2838,4 +2878,3 @@ def str2int(number):
|
||||||
# invalid number-word, raise ValueError
|
# invalid number-word, raise ValueError
|
||||||
raise ValueError(f"String {original_input} cannot be converted to int.")
|
raise ValueError(f"String {original_input} cannot be converted to int.")
|
||||||
return sum(sums)
|
return sum(sums)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue