diff --git a/evennia/utils/funcparser.py b/evennia/utils/funcparser.py index 6ed1b333ba..f401ed40ce 100644 --- a/evennia/utils/funcparser.py +++ b/evennia/utils/funcparser.py @@ -866,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 diff --git a/evennia/utils/tests/test_funcparser.py b/evennia/utils/tests/test_funcparser.py index c0c458ed70..713b8ebb2b 100644 --- a/evennia/utils/tests/test_funcparser.py +++ b/evennia/utils/tests/test_funcparser.py @@ -587,19 +587,35 @@ class TestDefaultCallables(TestCase): with self.assertRaises(TypeError): 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): """ Test choice callable, where output could be either choice. """ 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")) 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")) + 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) @@ -726,7 +742,6 @@ class TestCallableSearch(test_resources.BaseEvenniaTest): ret = self.parser.parse(string, caller=self.char1, return_str=False, raise_errors=True) self.assertEqual(expected, ret) - @unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916") def test_search_not_found(self): string = "$search(foo)" 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) 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) - @unittest.skip("broken, see https://github.com/evennia/evennia/issues/2916") def test_search_multiple_results_no_list(self): """ 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): 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): string = "Go to $search(Room)" with self.assertRaises(funcparser.ParsingError): diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 6c4b1a6df6..b2a9748299 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -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): if not inp: @@ -2570,16 +2578,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 +2603,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: @@ -2756,28 +2772,52 @@ def int2str(number, adjective=False): 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, + "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, + "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. """ @@ -2792,7 +2832,7 @@ def str2int(number): return int(number[:-2]) except: pass - + # convert sound changes for generic ordinal numbers if number[-2:] == "th": # remove "th" @@ -2812,10 +2852,10 @@ def str2int(number): return i # remove optional "and"s - number = number.replace(" and "," ") - + 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 ] + 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 @@ -2827,7 +2867,7 @@ def str2int(number): # 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 + sums[-1] = sums[-1] * i # otherwise, it's added on, like the "five" in "twenty five" else: sums.append(i) @@ -2838,4 +2878,3 @@ def str2int(number): # invalid number-word, raise ValueError raise ValueError(f"String {original_input} cannot be converted to int.") return sum(sums) -