diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 9852802e72..44540338f1 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -812,6 +812,65 @@ class TestJustify(TestCase): self.assertIn(ANSI_RED, str(result)) +class TestAtSearchResult(TestCase): + """ + Test the utils.at_search_result function. + + """ + + class MockObject: + def __init__(self, key): + self.key = key + self.aliases = '' + + def get_display_name(self, looker, **kwargs): + return self.key + + def get_extra_info(self, looker, **kwargs): + return '' + + def __repr__(self): + return f"MockObject({self.key})" + + def test_single_match(self): + """if there is only one match, it should return the matched object""" + obj1 = self.MockObject("obj1") + caller = mock.MagicMock() + self.assertEqual(obj1, utils.at_search_result([obj1], caller, "obj1")) + + def test_no_match(self): + """if there are no provided matches, the caller should receive the correct error message""" + caller = mock.MagicMock() + self.assertIsNone(utils.at_search_result([], caller, "obj1")) + caller.msg.assert_called_once_with("Could not find 'obj1'.") + + def test_basic_multimatch(self): + """multiple matches with the same name should return a message with incrementing indices""" + matches = [ self.MockObject("obj1") for _ in range(3) ] + caller = mock.MagicMock() + self.assertIsNone(utils.at_search_result(matches, caller, "obj1")) + multimatch_msg = """\ +More than one match for 'obj1' (please narrow target): + obj1-1 + obj1-2 + obj1-3""" + caller.msg.assert_called_once_with(multimatch_msg) + + def test_partial_multimatch(self): + """multiple partial matches with different names should increment index by unique name""" + matches = [ self.MockObject("obj1") for _ in range(3) ] + [ self.MockObject("obj2") for _ in range(2) ] + caller = mock.MagicMock() + self.assertIsNone(utils.at_search_result(matches, caller, "obj")) + multimatch_msg = """\ +More than one match for 'obj' (please narrow target): + obj1-1 + obj1-2 + obj1-3 + obj2-1 + obj2-2""" + caller.msg.assert_called_once_with(multimatch_msg) + + class TestGroupObjectsByKeyAndDesc(TestCase): """ Test the utils.group_objects_by_key_and_desc function. diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index cc92baee47..360213ba3a 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -2397,28 +2397,35 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs): query=query ) - for num, result in enumerate(matches): - # 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.db_key for alias in aliases if alias.db_category != "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) - if hasattr(result, "get_display_name") + # group results by display name to properly disambiguate + grouped_matches = defaultdict(list) + for item in matches: + group_key = ( + item.get_display_name(caller) + if hasattr(item, "get_display_name") else query - ), - aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "", - info=result.get_extra_info(caller), - ) + ) + grouped_matches[group_key].append(item) + + for key, match_list in grouped_matches.items(): + for num, result in enumerate(match_list): + # 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.db_key for alias in aliases if alias.db_category != "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=key, + aliases=" [{alias}]".format(alias=";".join(aliases)) if aliases else "", + info=result.get_extra_info(caller), + ) matches = None else: # exactly one match