mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 15:26:30 +01:00
Break up Object.search into multiple methods for easier overloading. Resolve #3417
This commit is contained in:
parent
32e9520db9
commit
015698d06f
7 changed files with 459 additions and 331 deletions
11
CHANGELOG.md
11
CHANGELOG.md
|
|
@ -2,17 +2,20 @@
|
|||
|
||||
## main branch
|
||||
|
||||
- [Feature] Add [`evennia.ON_DEMAND_HANDLER`][new-ondemandhandler] for making it
|
||||
- Feature: Add [`evennia.ON_DEMAND_HANDLER`][new-ondemandhandler] for making it
|
||||
easier to implement changes that are calculated on-demand (Griatch)
|
||||
- [Feature][pull3412]: Make it possible to add custom webclient css in
|
||||
`webclient/css/custom.css`, same as for website (InspectorCaracal)
|
||||
- [Feature][pull3367]: [Component contrib][pull3367extra] got better
|
||||
inheritance, slot names to choose attr storage, speedups and fixes (ChrisLR)
|
||||
- [Fix] Remove `AMP_ENABLED` setting since it services no real purpose and
|
||||
- Feature: Break up `DefaultObject.search` method into several helpers to make
|
||||
it easier to override (Griatch)
|
||||
- Fix: Resolve multimatch error with rpsystem contrib (Griatch)
|
||||
- Fix: Remove `AMP_ENABLED` setting since it services no real purpose and
|
||||
erroring out on setting it would make it even less useful (Griatch).
|
||||
- [Feature] Remove too-strict password restrictions for Evennia logins, using
|
||||
- Feature: Remove too-strict password restrictions for Evennia logins, using
|
||||
django defaults instead for passwords with more varied characters.
|
||||
- [Fix] `services` command with no args would traceback (regression) (Griatch)
|
||||
- Fix `services` command with no args would traceback (regression) (Griatch)
|
||||
- [Fix][pull3423]: Fix wilderness contrib error moving to an already existing
|
||||
wilderness room (InspectorCaracal)
|
||||
- [Fix][pull3425]: Don't always include example the crafting recipe when
|
||||
|
|
|
|||
|
|
@ -14,13 +14,10 @@ main test suite started with
|
|||
import datetime
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import evennia
|
||||
from anything import Anything
|
||||
from django.conf import settings
|
||||
from django.test import override_settings
|
||||
from parameterized import parameterized
|
||||
from twisted.internet import task
|
||||
|
||||
import evennia
|
||||
from evennia import (
|
||||
DefaultCharacter,
|
||||
DefaultExit,
|
||||
|
|
@ -32,14 +29,7 @@ from evennia import (
|
|||
from evennia.commands import cmdparser
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.command import Command, InterruptCommand
|
||||
from evennia.commands.default import (
|
||||
account,
|
||||
admin,
|
||||
batchprocess,
|
||||
building,
|
||||
comms,
|
||||
general,
|
||||
)
|
||||
from evennia.commands.default import account, admin, batchprocess, building, comms, general
|
||||
from evennia.commands.default import help as help_module
|
||||
from evennia.commands.default import syscommands, system, unloggedin
|
||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
|
|
@ -48,6 +38,8 @@ from evennia.prototypes import prototypes as protlib
|
|||
from evennia.utils import create, gametime, utils
|
||||
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
|
||||
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest
|
||||
from parameterized import parameterized
|
||||
from twisted.internet import task
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Command testing
|
||||
|
|
@ -149,14 +141,18 @@ class TestGeneral(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
CmdTest(),
|
||||
"/t",
|
||||
"test: Ambiguous switch supplied: "
|
||||
"Did you mean /test or /testswitch or /testswitch2?|Switches matched: []",
|
||||
(
|
||||
"test: Ambiguous switch supplied: "
|
||||
"Did you mean /test or /testswitch or /testswitch2?|Switches matched: []"
|
||||
),
|
||||
)
|
||||
self.call(
|
||||
CmdTest(),
|
||||
"/tests",
|
||||
"test: Ambiguous switch supplied: "
|
||||
"Did you mean /testswitch or /testswitch2?|Switches matched: []",
|
||||
(
|
||||
"test: Ambiguous switch supplied: "
|
||||
"Did you mean /testswitch or /testswitch2?|Switches matched: []"
|
||||
),
|
||||
)
|
||||
|
||||
def test_say(self):
|
||||
|
|
@ -846,8 +842,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdCpAttr(),
|
||||
"/copy Obj2/test2 = Obj2/test3",
|
||||
'@cpattr: Extra switch "/copy" ignored.|\nCopied Obj2.test2 -> Obj2.test3. '
|
||||
"(value: 'value2')",
|
||||
(
|
||||
'@cpattr: Extra switch "/copy" ignored.|\nCopied Obj2.test2 -> Obj2.test3. '
|
||||
"(value: 'value2')"
|
||||
),
|
||||
)
|
||||
self.call(building.CmdMvAttr(), "", "Usage: ")
|
||||
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
|
||||
|
|
@ -902,8 +900,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test1[5] =",
|
||||
"No attribute Obj/test1[5] [category: None] was found to "
|
||||
"delete. (Nested lookups attempted)",
|
||||
(
|
||||
"No attribute Obj/test1[5] [category: None] was found to "
|
||||
"delete. (Nested lookups attempted)"
|
||||
),
|
||||
)
|
||||
# Append
|
||||
self.call(
|
||||
|
|
@ -956,8 +956,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test2[+'three']",
|
||||
"Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups"
|
||||
" attempted)",
|
||||
(
|
||||
"Attribute Obj/test2[+'three'] [category:None] does not exist. (Nested lookups"
|
||||
" attempted)"
|
||||
),
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
|
|
@ -998,8 +1000,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test2['five'] =",
|
||||
"No attribute Obj/test2['five'] [category: None] "
|
||||
"was found to delete. (Nested lookups attempted)",
|
||||
(
|
||||
"No attribute Obj/test2['five'] [category: None] "
|
||||
"was found to delete. (Nested lookups attempted)"
|
||||
),
|
||||
)
|
||||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
|
|
@ -1009,8 +1013,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test2[+1]=33",
|
||||
"Modified attribute Obj/test2 [category:None] = "
|
||||
"{'one': 99, 'three': 3, '+': 42, '+1': 33}",
|
||||
(
|
||||
"Modified attribute Obj/test2 [category:None] = "
|
||||
"{'one': 99, 'three': 3, '+': 42, '+1': 33}"
|
||||
),
|
||||
)
|
||||
|
||||
# dict - case sensitive keys
|
||||
|
|
@ -1071,8 +1077,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
building.CmdSetAttribute(),
|
||||
# Special case for tuple, could have a better message
|
||||
"Obj/tup[1] = ",
|
||||
"No attribute Obj/tup[1] [category: None] "
|
||||
"was found to delete. (Nested lookups attempted)",
|
||||
(
|
||||
"No attribute Obj/tup[1] [category: None] "
|
||||
"was found to delete. (Nested lookups attempted)"
|
||||
),
|
||||
)
|
||||
|
||||
# Deaper nesting
|
||||
|
|
@ -1147,8 +1155,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSetAttribute(),
|
||||
"Obj/test4[0]['one']",
|
||||
"Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups"
|
||||
" attempted)",
|
||||
(
|
||||
"Attribute Obj/test4[0]['one'] [category:None] does not exist. (Nested lookups"
|
||||
" attempted)"
|
||||
),
|
||||
)
|
||||
|
||||
def test_split_nested_attr(self):
|
||||
|
|
@ -1255,8 +1265,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdDestroy(),
|
||||
"Obj",
|
||||
"Could not find 'Obj'.| (Objects to destroy "
|
||||
"must either be local or specified with a unique #dbref.)",
|
||||
(
|
||||
"Could not find 'Obj'.| (Objects to destroy "
|
||||
"must either be local or specified with a unique #dbref.)"
|
||||
),
|
||||
)
|
||||
settings.DEFAULT_HOME = f"#{self.room1.dbid}"
|
||||
self.call(
|
||||
|
|
@ -1376,14 +1388,18 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit.",
|
||||
(
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit."
|
||||
),
|
||||
)
|
||||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"Obj2 = evennia.objects.objects.DefaultExit",
|
||||
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit.",
|
||||
(
|
||||
"Obj2 changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit."
|
||||
),
|
||||
cmdstring="swap",
|
||||
inputs=["yes"],
|
||||
)
|
||||
|
|
@ -1396,8 +1412,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to"
|
||||
" override.",
|
||||
(
|
||||
"Obj already has the typeclass 'evennia.objects.objects.DefaultExit'. Use /force to"
|
||||
" override."
|
||||
),
|
||||
)
|
||||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
|
|
@ -1413,16 +1431,20 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"Obj",
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nOnly the"
|
||||
" at_object_creation hook was run (update mode). Attributes set before swap were not"
|
||||
" removed\n(use `swap` or `type/reset` to clear all).",
|
||||
(
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nOnly"
|
||||
" the at_object_creation hook was run (update mode). Attributes set before swap"
|
||||
" were not removed\n(use `swap` or `type/reset` to clear all)."
|
||||
),
|
||||
cmdstring="update",
|
||||
)
|
||||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"/reset/force Obj=evennia.objects.objects.DefaultObject",
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\n"
|
||||
"All object creation hooks were run. All old attributes where deleted before the swap.",
|
||||
(
|
||||
"Obj updated its existing typeclass (evennia.objects.objects.DefaultObject).\nAll"
|
||||
" object creation hooks were run. All old attributes where deleted before the swap."
|
||||
),
|
||||
inputs=["yes"],
|
||||
)
|
||||
|
||||
|
|
@ -1446,11 +1468,13 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"/prototype Obj=testkey",
|
||||
"replaced_obj changed typeclass from evennia.objects.objects.DefaultObject to "
|
||||
"typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
|
||||
"(update mode). Attributes set before swap were not removed\n"
|
||||
"(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
|
||||
"successfully applied over the object type.",
|
||||
(
|
||||
"replaced_obj changed typeclass from evennia.objects.objects.DefaultObject to "
|
||||
"typeclasses.objects.Object.\nOnly the at_object_creation hook was run "
|
||||
"(update mode). Attributes set before swap were not removed\n"
|
||||
"(use `swap` or `type/reset` to clear all). Prototype 'replaced_obj' was "
|
||||
"successfully applied over the object type."
|
||||
),
|
||||
)
|
||||
assert self.obj1.db.desc == "protdesc"
|
||||
|
||||
|
|
@ -1467,8 +1491,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdLock(),
|
||||
"/view Obj = edit:false()",
|
||||
"Switch(es) view can not be used with a lock assignment. "
|
||||
"Use e.g. lock/del objname/locktype instead.",
|
||||
(
|
||||
"Switch(es) view can not be used with a lock assignment. "
|
||||
"Use e.g. lock/del objname/locktype instead."
|
||||
),
|
||||
)
|
||||
self.call(building.CmdLock(), "Obj = control:false()")
|
||||
self.call(building.CmdLock(), "Obj = edit:false()")
|
||||
|
|
@ -1594,9 +1620,11 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdScripts(),
|
||||
"/delete #{}-#{}".format(script1.id, script3.id),
|
||||
f"Global Script Deleted - #{script1.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script2.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script3.id} (evennia.scripts.scripts.DefaultScript)",
|
||||
(
|
||||
f"Global Script Deleted - #{script1.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script2.id} (evennia.scripts.scripts.DefaultScript)|"
|
||||
f"Global Script Deleted - #{script3.id} (evennia.scripts.scripts.DefaultScript)"
|
||||
),
|
||||
inputs=["y"],
|
||||
)
|
||||
self.assertFalse(script1.pk)
|
||||
|
|
@ -1617,9 +1645,8 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdTeleport(),
|
||||
"Obj = Room2",
|
||||
"Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format(
|
||||
oid, rid, rid2
|
||||
),
|
||||
"Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2."
|
||||
.format(oid, rid, rid2),
|
||||
)
|
||||
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
|
||||
self.call(
|
||||
|
|
@ -1692,16 +1719,20 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
|
||||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
(
|
||||
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}"
|
||||
),
|
||||
"Saved prototype: testprot",
|
||||
inputs=["y"],
|
||||
)
|
||||
|
||||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/save testprot2 = {'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
(
|
||||
"/save testprot2 = {'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}"
|
||||
),
|
||||
"(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2",
|
||||
inputs=["y"],
|
||||
)
|
||||
|
|
@ -1712,8 +1743,10 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/save {'key':'Test Char', 'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"A prototype_key must be given, either as `prototype_key = <prototype>` or as "
|
||||
"a key 'prototype_key' inside the prototype structure.",
|
||||
(
|
||||
"A prototype_key must be given, either as `prototype_key = <prototype>` or as "
|
||||
"a key 'prototype_key' inside the prototype structure."
|
||||
),
|
||||
)
|
||||
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
|
@ -1735,7 +1768,8 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
|
||||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
||||
"'key':'goblin', 'location':'%s'}"
|
||||
% spawnLoc.dbref,
|
||||
"Spawned goblin",
|
||||
)
|
||||
goblin = get_object(self, "goblin")
|
||||
|
|
@ -1783,7 +1817,8 @@ class TestBuilding(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
|
||||
" 'location':'%s'}" % spawnLoc.dbref,
|
||||
" 'location':'%s'}"
|
||||
% spawnLoc.dbref,
|
||||
"Spawned Ball",
|
||||
)
|
||||
ball = get_object(self, "Ball")
|
||||
|
|
@ -2021,8 +2056,10 @@ class TestComms(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
comms.CmdPage(),
|
||||
"TestAccount2 = Test",
|
||||
"TestAccount2 is offline. They will see your message if they list their pages later."
|
||||
"|You paged TestAccount2 with: 'Test'.",
|
||||
(
|
||||
"TestAccount2 is offline. They will see your message if they list their pages"
|
||||
" later.|You paged TestAccount2 with: 'Test'."
|
||||
),
|
||||
receiver=self.account,
|
||||
)
|
||||
|
||||
|
|
@ -2095,8 +2132,10 @@ class TestBatchProcess(BaseEvenniaCommandTest):
|
|||
self.call(
|
||||
batchprocess.CmdBatchCommands(),
|
||||
"batchprocessor.example_batch_cmds_test",
|
||||
"Running Batch-command processor - Automatic mode for"
|
||||
" batchprocessor.example_batch_cmds",
|
||||
(
|
||||
"Running Batch-command processor - Automatic mode for"
|
||||
" batchprocessor.example_batch_cmds"
|
||||
),
|
||||
)
|
||||
# we make sure to delete the button again here to stop the running reactor
|
||||
confirm = building.CmdDestroy.confirm
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ from evennia.typeclasses.models import TypeclassBase
|
|||
from evennia.utils import ansi, create, funcparser, logger, search
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
dbref,
|
||||
is_iter,
|
||||
iter_to_str,
|
||||
lazy_property,
|
||||
|
|
@ -326,7 +327,213 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
return [exi for exi in self.contents if exi.destination]
|
||||
|
||||
# main methods
|
||||
# search methods and hooks
|
||||
|
||||
def get_search_query_replacement(self, searchdata, **kwargs):
|
||||
"""
|
||||
This method is called by the search method to allow for direct
|
||||
replacements of the search string before it is used in the search.
|
||||
|
||||
Args:
|
||||
searchdata (str): The search string to replace.
|
||||
**kwargs (any): These are the same as passed to the `search` method.
|
||||
|
||||
Returns:
|
||||
str: The (potentially modified) search string.
|
||||
|
||||
"""
|
||||
if kwargs.get("use_nicks"):
|
||||
return self.nicks.nickreplace(
|
||||
searchdata, categories=("object", "account"), include_account=True
|
||||
)
|
||||
return searchdata
|
||||
|
||||
def get_search_direct_match(self, searchdata, **kwargs):
|
||||
"""
|
||||
This method is called by the search method to allow for direct
|
||||
replacements, such as 'me' always being an alias for this object.
|
||||
|
||||
Args:
|
||||
searchdata (str): The search string to replace.
|
||||
**kwargs (any): These are the same as passed to the `search` method.
|
||||
|
||||
Returns:
|
||||
tuple: `(should_return, str or Obj)`, where `should_return` is a boolean indicating
|
||||
the `.search` method should return the result immediately without further
|
||||
processing. If `should_return` is `True`, the second element of the tuple is the result
|
||||
that is returned.
|
||||
|
||||
"""
|
||||
if isinstance(searchdata, str):
|
||||
match searchdata.lower():
|
||||
case "me" | "self":
|
||||
return True, self
|
||||
case "here":
|
||||
return True, self.location
|
||||
return False, searchdata
|
||||
|
||||
def get_search_candidates(self, searchdata, **kwargs):
|
||||
"""
|
||||
Get the candidates for a search. Also the `candidates` provided to the
|
||||
search function is included, and could be modified in-place here.
|
||||
|
||||
Args:
|
||||
searchdata (str): The search criterion (could be modified by `get_search_query_replacement`).
|
||||
**kwargs (any): These are the same as passed to the `search` method.
|
||||
|
||||
Returns:
|
||||
list: A list of objects to search between.
|
||||
|
||||
Notes:
|
||||
If `searchdata` is a #dbref, this method should always return `None`. This is because
|
||||
the search should always be global in this case. If `candidates` were already given,
|
||||
they should be used as is. If `location` was given, the candidates should be based on
|
||||
that.
|
||||
|
||||
"""
|
||||
if kwargs.get("global_search") or dbref(searchdata):
|
||||
# global searches (dbref-searches are always global too) should not have any candidates
|
||||
return None
|
||||
|
||||
# if candidates were already given, use them
|
||||
candidates = kwargs.get("candidates")
|
||||
if candidates:
|
||||
return candidates
|
||||
|
||||
# find candidates based on location
|
||||
location = kwargs.get("location")
|
||||
|
||||
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)
|
||||
return candidates
|
||||
|
||||
def get_search_result(
|
||||
self,
|
||||
searchdata,
|
||||
attribute_name=None,
|
||||
typeclass=None,
|
||||
candidates=None,
|
||||
exact=False,
|
||||
use_dbref=None,
|
||||
tags=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
This is a wrapper for actually searching for objects, used by the `search` method.
|
||||
This is broken out into a separate method to allow for easier overriding in child classes.
|
||||
|
||||
Args:
|
||||
searchdata (str): The search criterion.
|
||||
attribute_name (str): The attribute to search on (default is `.
|
||||
typeclass (Typeclass or list): The typeclass to search for.
|
||||
candidates (list): A list of objects to search between.
|
||||
exact (bool): Require exact match.
|
||||
use_dbref (bool): Allow dbref search.
|
||||
tags (list): Tags to search for.
|
||||
|
||||
"""
|
||||
|
||||
return ObjectDB.objects.search_object(
|
||||
searchdata,
|
||||
attribute_name=attribute_name,
|
||||
typeclass=typeclass,
|
||||
candidates=candidates,
|
||||
exact=exact,
|
||||
use_dbref=use_dbref,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
def get_stacked_results(self, results, **kwargs):
|
||||
"""
|
||||
This method is called by the search method to allow for handling of multi-match
|
||||
results that should be stacked.
|
||||
|
||||
Args:
|
||||
results (list): The list of results from the search.
|
||||
|
||||
Returns:
|
||||
tuple: `(stacked, results)`, where `stacked` is a boolean indicating if the
|
||||
result is stacked and `results` is the list of results to return. If `stacked`
|
||||
is True, the ".search" method will return `results` immediately without further
|
||||
processing (it will not result in a multimatch-error).
|
||||
|
||||
Notes:
|
||||
The `stacked` keyword argument is an integer that controls the max size of each stack
|
||||
(if >0). It's important to make sure to only stack _identical_ objects, otherwise we
|
||||
risk losing track of objects.
|
||||
|
||||
"""
|
||||
nresults = len(results)
|
||||
max_stack_size = kwargs.get("stacked", 0)
|
||||
typeclass = kwargs.get("typeclass")
|
||||
exact = kwargs.get("exact", False)
|
||||
|
||||
if max_stack_size > 0 and nresults > 1:
|
||||
nstack = nresults
|
||||
if not exact:
|
||||
# we re-run exact match against one of the matches to make sure all are indeed
|
||||
# equal and we were not catching partial matches not belonging to the stack
|
||||
nstack = len(
|
||||
ObjectDB.objects.get_objs_with_key_or_alias(
|
||||
results[0].key,
|
||||
exact=True,
|
||||
candidates=list(results),
|
||||
typeclasses=[typeclass] if typeclass else None,
|
||||
)
|
||||
)
|
||||
if nstack == nresults:
|
||||
# a valid stack of identical items, return multiple results
|
||||
return True, list(results)[:max_stack_size]
|
||||
|
||||
return False, results
|
||||
|
||||
def handle_search_results(self, searchdata, results, **kwargs):
|
||||
"""
|
||||
This method is called by the search method to allow for handling of the final search result.
|
||||
|
||||
Args:
|
||||
searchdata (str): The original search criterion (potentially modified by
|
||||
`get_search_query_replacement`).
|
||||
results (list): The list of results from the search.
|
||||
**kwargs (any): These are the same as passed to the `search` method.
|
||||
|
||||
Returns:
|
||||
Object, None or list: Normally this is a single object, but if `quiet=True` it should be
|
||||
a list. If quiet=False and we have to handle a no/multi-match error (directly messaging
|
||||
the user), this should return `None`.
|
||||
|
||||
"""
|
||||
if kwargs.get("quiet"):
|
||||
# don't care about no/multi-match errors, just return list of whatever we have
|
||||
return list(results)
|
||||
|
||||
# handle any error messages, otherwise return a single result
|
||||
|
||||
nofound_string = kwargs.get("nofound_string")
|
||||
multimatch_string = kwargs.get("multimatch_string")
|
||||
|
||||
return _AT_SEARCH_RESULT(
|
||||
results,
|
||||
self,
|
||||
query=searchdata,
|
||||
nofound_string=nofound_string,
|
||||
multimatch_string=multimatch_string,
|
||||
)
|
||||
|
||||
def search(
|
||||
self,
|
||||
|
|
@ -411,10 +618,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
more tag definitions on the form `tagname` or `(tagname, tagcategory)`.
|
||||
stacked (int, optional): If > 0, multimatches will be analyzed to determine if they
|
||||
only contains identical objects; these are then assumed 'stacked' and no multi-match
|
||||
error will be generated, instead `stacked` number of matches will be returned. If
|
||||
`stacked` is larger than number of matches, returns that number of matches. If
|
||||
the found stack is a mix of objects, return None and handle the multi-match
|
||||
error depending on the value of `quiet`.
|
||||
error will be generated, instead `stacked` number of matches will be returned as a
|
||||
list. If `stacked` is larger than number of matches, returns that number of matches.
|
||||
If the found stack is a mix of objects, return None and handle the multi-match error
|
||||
depending on the value of `quiet`.
|
||||
|
||||
Returns:
|
||||
Object, None or list: Will return an `Object` or `None` if `quiet=False`. Will return
|
||||
|
|
@ -429,59 +636,40 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
messaging is assumed to be handled by the caller.
|
||||
|
||||
"""
|
||||
is_string = isinstance(searchdata, str)
|
||||
# store input kwargs for sub-methods (this must be done first in this method)
|
||||
input_kwargs = {
|
||||
key: value for key, value in locals().items() if key not in ("self", "searchdata")
|
||||
}
|
||||
|
||||
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
|
||||
# replace incoming searchdata string with a potentially modified version
|
||||
searchdata = self.get_search_query_replacement(searchdata, **input_kwargs)
|
||||
|
||||
if use_dbref is None:
|
||||
use_dbref = self.locks.check_lockstring(self, "_dummy:perm(Builder)")
|
||||
# handle special input strings, like "me" or "here".
|
||||
should_return, searchdata = self.get_search_direct_match(searchdata, **input_kwargs)
|
||||
if should_return:
|
||||
# we got an actual result, return it immediately
|
||||
return [searchdata] if quiet else searchdata
|
||||
|
||||
if use_nicks:
|
||||
# do nick-replacement on search
|
||||
searchdata = self.nicks.nickreplace(
|
||||
searchdata, categories=("object", "account"), include_account=True
|
||||
)
|
||||
# if use_dbref is None, we use a lock to determine if dbref search is allowed
|
||||
use_dbref = (
|
||||
self.locks.check_lockstring(self, "_dummy:perm(Builder)")
|
||||
if use_dbref is None
|
||||
else use_dbref
|
||||
)
|
||||
|
||||
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
|
||||
candidates = None
|
||||
# convert tags into tag tuples suitable for query
|
||||
tags = [
|
||||
(tagkey, tagcat[0] if tagcat else None) for tagkey, *tagcat in make_iter(tags or [])
|
||||
]
|
||||
|
||||
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)
|
||||
# always use exact match for dbref/global searches
|
||||
exact = True if global_search or dbref(searchdata) else exact
|
||||
|
||||
if tags:
|
||||
tags = [(tagkey, tagcat[0] if tagcat else None) for tagkey, *tagcat in make_iter(tags)]
|
||||
# get candidates
|
||||
candidates = self.get_search_candidates(searchdata, **input_kwargs)
|
||||
|
||||
results = ObjectDB.objects.search_object(
|
||||
# do the actual search
|
||||
results = self.get_search_result(
|
||||
searchdata,
|
||||
attribute_name=attribute_name,
|
||||
typeclass=typeclass,
|
||||
|
|
@ -491,41 +679,18 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
tags=tags,
|
||||
)
|
||||
|
||||
# filter out objects we are not allowed to search
|
||||
if use_locks:
|
||||
results = [x for x in list(results) if x.access(self, "search", default=True)]
|
||||
|
||||
nresults = len(results)
|
||||
if stacked > 0 and nresults > 1:
|
||||
# handle stacks, disable multimatch errors
|
||||
nstack = nresults
|
||||
if not exact:
|
||||
# we re-run exact match against one of the matches to
|
||||
# make sure we were not catching partial matches not belonging
|
||||
# to the stack
|
||||
nstack = len(
|
||||
ObjectDB.objects.get_objs_with_key_or_alias(
|
||||
results[0].key,
|
||||
exact=True,
|
||||
candidates=list(results),
|
||||
typeclasses=[typeclass] if typeclass else None,
|
||||
)
|
||||
)
|
||||
if nstack == nresults:
|
||||
# a valid stack, return multiple results
|
||||
return list(results)[:stacked]
|
||||
# handle stacked objects
|
||||
is_stacked, results = self.get_stacked_results(results, **input_kwargs)
|
||||
if is_stacked:
|
||||
# we have a stacked result, return it immediately (a list)
|
||||
return results
|
||||
|
||||
if quiet:
|
||||
# don't auto-handle error messaging
|
||||
return list(results)
|
||||
|
||||
# handle error messages
|
||||
return _AT_SEARCH_RESULT(
|
||||
results,
|
||||
self,
|
||||
query=searchdata,
|
||||
nofound_string=nofound_string,
|
||||
multimatch_string=multimatch_string,
|
||||
)
|
||||
# handle the end (unstacked) results, returning a single object, a list or None
|
||||
return self.handle_search_results(searchdata, results, **input_kwargs)
|
||||
|
||||
def search_account(self, searchdata, quiet=False):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -115,25 +115,41 @@ class DefaultObjectTest(BaseEvenniaTest):
|
|||
self.assertEqual(self.char1.search("co", stacked=2), None)
|
||||
|
||||
def test_get_default_lockstring_base(self):
|
||||
pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
pattern = (
|
||||
f"control:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
)
|
||||
self.assertEqual(
|
||||
DefaultObject.get_default_lockstring(account=self.account, caller=self.char1), pattern
|
||||
)
|
||||
|
||||
def test_get_default_lockstring_room(self):
|
||||
pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
pattern = (
|
||||
f"control:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
)
|
||||
self.assertEqual(
|
||||
DefaultRoom.get_default_lockstring(account=self.account, caller=self.char1), pattern
|
||||
)
|
||||
|
||||
def test_get_default_lockstring_exit(self):
|
||||
pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
pattern = (
|
||||
f"control:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or"
|
||||
f" perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)"
|
||||
)
|
||||
self.assertEqual(
|
||||
DefaultExit.get_default_lockstring(account=self.account, caller=self.char1), pattern
|
||||
)
|
||||
|
||||
def test_get_default_lockstring_character(self):
|
||||
pattern = f"puppet:pid({self.account.id}) or perm(Developer) or pperm(Developer);delete:pid({self.account.id}) or perm(Admin);edit:pid({self.account.id}) or perm(Admin)"
|
||||
pattern = (
|
||||
f"puppet:pid({self.account.id}) or perm(Developer) or"
|
||||
f" pperm(Developer);delete:pid({self.account.id}) or"
|
||||
f" perm(Admin);edit:pid({self.account.id}) or perm(Admin)"
|
||||
)
|
||||
self.assertEqual(
|
||||
DefaultCharacter.get_default_lockstring(account=self.account, caller=self.char1),
|
||||
pattern,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,7 @@ import unittest
|
|||
|
||||
from django.test import TestCase
|
||||
from django.test.runner import DiscoverRunner
|
||||
|
||||
from evennia.server.throttle import Throttle
|
||||
from evennia.server.validators import EvenniaPasswordValidator
|
||||
from evennia.utils.test_resources import BaseEvenniaTest
|
||||
|
||||
from ..deprecations import check_errors
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue