Break up Object.search into multiple methods for easier overloading. Resolve #3417

This commit is contained in:
Griatch 2024-02-25 16:37:17 +01:00
parent 32e9520db9
commit 015698d06f
7 changed files with 459 additions and 331 deletions

View file

@ -154,18 +154,12 @@ from string import punctuation
import inflect
from django.conf import settings
from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command
from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.utils import ansi, logger
from evennia.utils.utils import (
iter_to_str,
lazy_property,
make_iter,
variable_from_module,
)
from evennia.utils.utils import iter_to_str, lazy_property, make_iter, variable_from_module
_INFLECT = inflect.engine()
@ -179,7 +173,7 @@ _AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".",
# The prefix is the (single-character) symbol used to find the start
# of a object reference, such as /tall (note that
# the system will understand multi-word references like '/a tall man' too).
_PREFIX = getattr(settings, 'RPSYSTEM_EMOTE_PREFIX', "/")
_PREFIX = getattr(settings, "RPSYSTEM_EMOTE_PREFIX", "/")
# The num_sep is the (single-character) symbol used to separate the
# sdesc from the number when trying to separate identical sdescs from
@ -1317,145 +1311,25 @@ class ContribRPObject(DefaultObject):
self.db.pose_default = "is here."
self.db._sdesc = ""
def search(
def get_search_result(
self,
searchdata,
global_search=False,
use_nicks=True,
typeclass=None,
location=None,
attribute_name=None,
quiet=False,
exact=False,
typeclass=None,
candidates=None,
nofound_string=None,
multimatch_string=None,
exact=False,
use_dbref=None,
tags=None,
**kwargs,
):
"""
Returns an Object matching a search string/condition, taking
sdescs into account.
Perform a standard object search in the database, handling
multiple results and lack thereof gracefully. By default, only
objects in the current `location` of `self` or its inventory are searched for.
Args:
searchdata (str or obj): Primary search criterion. Will be matched
against `object.key` (with `object.aliases` second) unless
the keyword attribute_name specifies otherwise.
**Special strings:**
- `#<num>`: search by unique dbref. This is always
a global search.
- `me,self`: self-reference to this object
- `<num>-<string>` - can be used to differentiate
between multiple same-named matches
global_search (bool): Search all objects globally. This is overruled
by `location` keyword.
use_nicks (bool): Use nickname-replace (nicktype "object") on `searchdata`.
typeclass (str or Typeclass, or list of either): Limit search only
to `Objects` with this typeclass. May be a list of typeclasses
for a broader search.
location (Object or list): Specify a location or multiple locations
to search. Note that this is used to query the *contents* of a
location and will not match for the location itself -
if you want that, don't set this or use `candidates` to specify
exactly which objects should be searched.
attribute_name (str): Define which property to search. If set, no
key+alias search will be performed. This can be used
to search database fields (db_ will be automatically
appended), and if that fails, it will try to return
objects having Attributes with this name and value
equal to searchdata. A special use is to search for
"key" here if you want to do a key-search without
including aliases.
quiet (bool): don't display default error messages - this tells the
search method that the user wants to handle all errors
themselves. It also changes the return value type, see
below.
exact (bool): if unset (default) - prefers to match to beginning of
string rather than not matching at all. If set, requires
exact matching of entire string.
candidates (list of objects): this is an optional custom list of objects
to search (filter) between. It is ignored if `global_search`
is given. If not set, this list will automatically be defined
to include the location, the contents of location and the
caller's contents (inventory).
nofound_string (str): optional custom string for not-found error message.
multimatch_string (str): optional custom string for multimatch error header.
use_dbref (bool or None): If None, only turn off use_dbref if we are of a lower
permission than Builder. Otherwise, honor the True/False value.
Returns:
match (Object, None or list): will return an Object/None if `quiet=False`,
otherwise it will return a list of 0, 1 or more matches.
Notes:
To find Accounts, use eg. `evennia.account_search`. If
`quiet=False`, error messages will be handled by
`settings.SEARCH_AT_RESULT` and echoed automatically (on
error, return will be `None`). If `quiet=True`, the error
messaging is assumed to be handled by the caller.
Override of the parent method for producing search results that understands sdescs.
These are used in the main .search() method of the parent class.
"""
is_string = isinstance(searchdata, str)
if is_string:
# searchdata is a string; wrap some common self-references
if searchdata.lower() in ("here",):
return [self.location] if quiet else self.location
if searchdata.lower() in ("me", "self"):
return [self] if quiet else self
if use_nicks:
# do nick-replacement on search
searchdata = self.nicks.nickreplace(
searchdata, categories=("object", "account"), include_account=True
)
if global_search or (
is_string
and searchdata.startswith("#")
and len(searchdata) > 1
and searchdata[1:].isdigit()
):
# only allow exact matching if searching the entire database
# or unique #dbrefs
exact = True
elif candidates is None:
# no custom candidates given - get them automatically
if location:
# location(s) were given
candidates = []
for obj in make_iter(location):
candidates.extend(obj.contents)
else:
# local search. Candidates are taken from
# self.contents, self.location and
# self.location.contents
location = self.location
candidates = self.contents
if location:
candidates = candidates + [location] + location.contents
else:
# normally we don't need this since we are
# included in location.contents
candidates.append(self)
# the sdesc-related substitution
# we also want to use the default search method
search_obj = super().get_search_result
is_builder = self.locks.check_lockstring(self, "perm(Builder)")
use_dbref = is_builder if use_dbref is None else use_dbref
def search_obj(string):
"helper wrapper for searching"
return ObjectDB.objects.object_search(
string,
attribute_name=attribute_name,
typeclass=typeclass,
candidates=candidates,
exact=exact,
use_dbref=use_dbref,
)
if candidates:
candidates = parse_sdescs_and_recogs(
@ -1478,16 +1352,7 @@ class ContribRPObject(DefaultObject):
# only done in code, so is controlled, #dbrefs are turned off
# for non-Builders.
results = search_obj(searchdata)
if quiet:
return results
return _AT_SEARCH_RESULT(
results,
self,
query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string,
)
return results
def get_posed_sdesc(self, sdesc, **kwargs):
"""

View file

@ -5,8 +5,7 @@ Tests for RP system
import time
from anything import Anything
from evennia import create_object
from evennia import DefaultObject, create_object, default_cmds
from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest
@ -157,12 +156,11 @@ class TestRPSystem(BaseEvenniaTest):
self.assertEqual(
rpsystem.parse_language(self.speaker, language_emote),
(
'For a change of pace, /me says, {##0}',
{"##0": ('elvish', '"This is in elvish!"')},
"For a change of pace, /me says, {##0}",
{"##0": ("elvish", '"This is in elvish!"')},
),
)
def test_parse_sdescs_and_recogs(self):
speaker = self.speaker
speaker.sdesc.add(sdesc0)
@ -251,18 +249,24 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
self.assertEqual(
self.out0[0],
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
(
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n'
),
)
self.assertEqual(
self.out1[0],
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
(
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n'
),
)
self.assertEqual(
self.out2[0],
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
(
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n'
),
)
def test_send_emote_fallback(self):
@ -287,8 +291,10 @@ class TestRPSystem(BaseEvenniaTest):
)
self.assertEqual(
self.out2[0],
"|bA nice sender of emotes|n is distracted from |bthe first receiver of emotes.|n by"
" something.",
(
"|bA nice sender of emotes|n is distracted from |bthe first receiver of emotes.|n"
" by something."
),
)
def test_send_case_sensitive_emote(self):
@ -306,21 +312,27 @@ class TestRPSystem(BaseEvenniaTest):
rpsystem.send_emote(speaker, receivers, case_emote)
self.assertEqual(
self.out0[0],
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
(
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice."
),
)
self.assertEqual(
self.out1[0],
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
(
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
"and |bAnother nice colliding sdesc-guy for tests|n twice."
),
)
self.assertEqual(
self.out2[0],
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
(
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
"|bThe first receiver of emotes.|n and |mReceiver2|n twice."
),
)
def test_rpsearch(self):
@ -371,8 +383,10 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
self.call(
rpsystem.CmdRecog(),
"",
"Currently recognized (use 'recog <sdesc> as <alias>' to add new "
"and 'forget <alias>' to remove):\n friend (BarFoo Character)",
(
"Currently recognized (use 'recog <sdesc> as <alias>' to add new "
"and 'forget <alias>' to remove):\n friend (BarFoo Character)"
),
)
self.call(
rpsystem.CmdRecog(),
@ -382,3 +396,31 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
)
self.call(rpsystem.CmdSdesc(), "clear", 'Cleared sdesc, using name "Char".', inputs=["Y"])
def test_multi_match_search(self):
"""
Test that the multi-match search works as expected
"""
mushroom1 = create_object(rpsystem.ContribRPObject, key="Mushroom", location=self.room1)
mushroom1.db.desc = "The first mushroom is brown."
mushroom2 = create_object(rpsystem.ContribRPObject, key="Mushroom", location=self.room1)
mushroom2.db.desc = "The second mushroom is red."
# check locations and contents
self.assertEqual(self.char1.location, self.room1)
self.assertTrue(set(self.room1.contents).intersection(set([mushroom1, mushroom2])))
expected_first_call = [
"More than one match for 'Mushroom' (please narrow target):",
f" Mushroom({mushroom1.dbref})-1 []",
f" Mushroom({mushroom2.dbref})-2 []",
]
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES
expected_second_call = f"Mushroom({mushroom1.dbref})\nThe first mushroom is brown."
self.call(default_cmds.CmdLook(), "Mushroom-1", expected_second_call) # FAILS
expected_third_call = f"Mushroom({mushroom2.dbref})\nThe second mushroom is red."
self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS