Fixed and debugged object_search to more generically search for any attribute name except 'key' (issue110). Also included alias-search as a last-resort if normal searches fail. This is now also working for global searches (issue106).

This commit is contained in:
Griatch 2010-09-12 15:07:12 +00:00
parent 7e736d19e2
commit 8bedd4d793
5 changed files with 184 additions and 176 deletions

View file

@ -55,7 +55,7 @@ class ObjManipCommand(MuxCommand):
if ';' in objdef: if ';' in objdef:
objdef, aliases = [str(part).strip() objdef, aliases = [str(part).strip()
for part in objdef.split(';', 1)] for part in objdef.split(';', 1)]
aliases = [str(alias).strip().lower() aliases = [str(alias).strip()
for alias in aliases.split(';') if alias.strip()] for alias in aliases.split(';') if alias.strip()]
lhs_objs.append({"name":objdef, lhs_objs.append({"name":objdef,
'option': option, 'aliases': aliases}) 'option': option, 'aliases': aliases})
@ -70,7 +70,7 @@ class ObjManipCommand(MuxCommand):
if ';' in objdef: if ';' in objdef:
objdef, aliases = [str(part).strip() objdef, aliases = [str(part).strip()
for part in objdef.split(';', 1)] for part in objdef.split(';', 1)]
aliases = [str(alias).strip().lower() aliases = [str(alias).strip()
for alias in aliases.split(';') if alias.strip()] for alias in aliases.split(';') if alias.strip()]
rhs_objs.append({"name":objdef, 'option': option, 'aliases': aliases}) rhs_objs.append({"name":objdef, 'option': option, 'aliases': aliases})
@ -1026,7 +1026,7 @@ class CmdDig(ObjManipCommand):
caller = self.caller caller = self.caller
if not self.lhs: if not self.lhs:
string = "Usage: @dig[/teleport] roomname[:parent] [= exit_there" string = "Usage: @dig[/teleport] roomname[;alias;alias...][:parent] [= exit_there"
string += "[;alias;alias..][:parent]] " string += "[;alias;alias..][:parent]] "
string += "[, exit_back_here[;alias;alias..][:parent]]" string += "[, exit_back_here[;alias;alias..][:parent]]"
caller.msg(string) caller.msg(string)
@ -1426,7 +1426,6 @@ class CmdExamine(ObjManipCommand):
text = "%s[...]" % text[:line_width - headlen - 5] text = "%s[...]" % text[:line_width - headlen - 5]
return text return text
def format_attributes(self, obj, attrname=None): def format_attributes(self, obj, attrname=None):
""" """
Helper function that returns info about attributes and/or Helper function that returns info about attributes and/or

View file

@ -15,7 +15,7 @@ except Exception:
from src.objects.object_search_funcs import object_multimatch_parser as IDPARSER from src.objects.object_search_funcs import object_multimatch_parser as IDPARSER
# #
# Helper function for the ObjectManger's search methods # Helper functions for the ObjectManger's search methods
# #
def match_list(searchlist, ostring, exact_match=True, def match_list(searchlist, ostring, exact_match=True,
@ -24,30 +24,95 @@ def match_list(searchlist, ostring, exact_match=True,
Helper function. Helper function.
does name/attribute matching through a list of objects. does name/attribute matching through a list of objects.
""" """
ostring = ostring.lower()
if attribute_name:
#search an arbitrary attribute name for a value match.
if exact_match:
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring == str(getattr(prospect, attribute_name)).lower()) \
or (ostring == str(prospect.get_attribute(attribute_name)).lower())]
else:
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring in str(getattr(prospect, attribute_name)).lower()) \
or (ostring in (str(p).lower() for p in prospect.get_attribute(attribute_name)))]
else:
#search the default "key" attribute
if not ostring:
return []
if not attribute_name:
attribute_name = "key"
if isinstance(ostring, basestring):
# strings are case-insensitive
ostring = ostring.lower()
if exact_match: if exact_match:
return [prospect for prospect in searchlist return [prospect for prospect in searchlist
if ostring == str(prospect.key).lower()] if (hasattr(prospect, attribute_name) and
ostring == str(getattr(prospect, attribute_name)).lower())
or (prospect.has_attribute(attribute_name) and
ostring == str(prospect.get_attribute(attribute_name)).lower())]
else: else:
return [prospect for prospect in searchlist return [prospect for prospect in searchlist
if ostring in str(prospect.key).lower()] if (hasattr(prospect, attribute_name) and
ostring in str(getattr(prospect, attribute_name)).lower())
or (prospect.has_attribute(attribute_name) and
ostring in str(prospect.get_attribute(attribute_name)).lower())]
else:
# If it's not a string, we don't convert to lowercase. This is also
# always treated as an exact match.
return [prospect for prospect in searchlist
if (hasattr(prospect, attribute_name) and
ostring == getattr(prospect, attribute_name))
or (prospect.has_attribute(attribute_name)
and ostring == prospect.get_attribute(attribute_name))]
def separable_search(ostring, searchlist,
attribute_name='db_key', exact_match=False):
"""
Searches a list for a object match to ostring or separator+keywords.
This version handles search criteria defined by IDPARSER. By default this
is of the type N-keyword, used to differentiate several objects of the
exact same name, e.g. 1-box, 2-box etc.
ostring: (string) The string to match against.
searchlist: (List of Objects) The objects to perform attribute comparisons on.
attribute_name: (string) attribute name to search.
exact_match: (bool) 'exact' or 'fuzzy' matching.
Note that the fuzzy matching gives precedence to exact matches; so if your
search query matches an object in the list exactly, it will be the only result.
This means that if the list contains [box,box11,box12], the search string 'box'
will only match the first entry since it is exact. The search 'box1' will however
match both box11 and box12 since neither is an exact match.
This method always returns a list, also for a single result.
"""
# Full search - this may return multiple matches.
results = match_list(searchlist, ostring, exact_match, attribute_name)
# Deal with results of search
match_number = None
if not results:
# if we have no match, check if we are dealing
# with a "N-keyword" query, if so, strip it out.
match_number, ostring = IDPARSER(ostring)
if match_number != None and ostring:
# Run the search again, without the match number
results = match_list(searchlist, ostring, exact_match, attribute_name)
elif not exact_match:
# we have results, but are using fuzzy matching; run
# second sweep in results to catch eventual exact matches
# (these are given precedence, so a search for 'ball' in
# ['ball', 'ball2'] will correctly return the first ball
# only).
exact_results = match_list(results, ostring, True, attribute_name)
if exact_results:
results = exact_results
if len(results) > 1 and match_number != None:
# We have multiple matches, but a N-type match number
# is available to separate them.
try:
results = [results[match_number]]
except IndexError:
pass
# this is always a list.
return results
class ObjectManager(TypedObjectManager): class ObjectManager(TypedObjectManager):
""" """
This is the main ObjectManager for all in-game objects. It This is the main ObjectManager for all in-game objects. It
@ -61,6 +126,9 @@ class ObjectManager(TypedObjectManager):
# #
# ObjectManager Get methods # ObjectManager Get methods
# #
# user/player related
@returns_typeclass @returns_typeclass
def get_object_with_user(self, user): def get_object_with_user(self, user):
@ -81,7 +149,7 @@ class ObjectManager(TypedObjectManager):
return None return None
# This returns typeclass since get_object_with_user and get_dbref does. # This returns typeclass since get_object_with_user and get_dbref does.
def player_name_search(self, search_string): def get_object_with_player(self, search_string):
""" """
Search for an object based on its player's name or dbref. Search for an object based on its player's name or dbref.
This search This search
@ -89,21 +157,26 @@ class ObjectManager(TypedObjectManager):
the search criterion (e.g. in local_and_global_search). the search criterion (e.g. in local_and_global_search).
search_string: (string) The name or dbref to search for. search_string: (string) The name or dbref to search for.
""" """
search_string = str(search_string).lstrip('*') search_string = str(search_string).lstrip('*')
dbref = self.dbref(search_string) dbref = self.dbref(search_string)
if dbref: if not dbref:
# this is a valid dbref. Try to match it. # not a dbref. Search by name.
dbref_match = self.dbref_search(dbref) player_matches = User.objects.filter(username__iexact=search_string)
if dbref_match: if player_matches:
return dbref_match dbref = player_matches[0].id
# use the id to find the player
return self.get_object_with_user(dbref)
# not a dbref. Search by name. # attr/property related
player_matches = User.objects.filter(username__iexact=search_string)
if player_matches: @returns_typeclass_list
uid = player_matches[0].id def get_objs_with_attr(self, attribute_name):
return self.get_object_with_user(uid) """
return None Returns all objects having the given attribute_name defined at all.
"""
from src.objects.models import ObjAttribute
return [attr.obj for attr in ObjAttribute.objects.filter(db_key=attribute_name)]
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_attr_match(self, attribute_name, attribute_value): def get_objs_with_attr_match(self, attribute_name, attribute_value):
@ -111,20 +184,32 @@ class ObjectManager(TypedObjectManager):
Returns all objects having the valid Returns all objects having the valid
attrname set to the given value. Note that no conversion is made attrname set to the given value. Note that no conversion is made
to attribute_value, and so it can accept also non-strings. to attribute_value, and so it can accept also non-strings.
""" """
from src.objects.models import ObjAttribute
return [prospect for prospect in self.all() return [attr.obj for attr in ObjAttribute.objects.filter(db_key=attribute_name)
if attribute_value if attribute_value == attr.value]
and attribute_value == prospect.get_attribute(attribute_name)]
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_attr(self, attribute_name): def get_objs_with_db_property(self, property_name):
""" """
Returns all objects having the given attribute_name defined at all. Returns all objects having a given db field property
""" """
return [prospect for prospect in self.all() return [prospect for prospect in self.all()
if prospect.get_attribute(attribute_name)] if hasattr(prospect, 'db_%s' % property_name)
or hasattr(prospect, property_name)]
@returns_typeclass_list
def get_objs_with_db_property_match(self, property_name, property_value):
"""
Returns all objects having a given db field property
"""
try:
return eval("self.filter(db_%s=%s)" % (property_name, property_value))
except Exception:
return []
# main search methods and helper functions
@returns_typeclass_list @returns_typeclass_list
def get_contents(self, location, excludeobj=None): def get_contents(self, location, excludeobj=None):
""" """
@ -147,94 +232,6 @@ class ObjectManager(TypedObjectManager):
ostring in obj.aliases): ostring in obj.aliases):
matches.append(obj) matches.append(obj)
return matches return matches
@returns_typeclass_list
def separable_search(self, ostring, searchlist=None,
attribute_name=None, exact_match=False):
"""
Searches for a object hit for ostring.
This version handles search criteria of the type N-keyword, this is used
to differentiate several objects of the exact same name, e.g. 1-box, 2-box etc.
ostring: (string) The string to match against.
searchlist: (List of Objects) The objects to perform name comparisons on.
if not given, will search the database normally.
attribute_name: (string) attribute name to search, if None, object key is used.
exact_match: (bool) 'exact' or 'fuzzy' matching.
Note that the fuzzy matching gives precedence to exact matches; so if your
search query matches an object in the list exactly, it will be the only result.
This means that if the list contains [box,box11,box12], the search string 'box'
will only match the first entry since it is exact. The search 'box1' will however
match both box11 and box12 since neither is an exact match.
This method always returns a list, also for a single result.
"""
def run_dbref_search(ostring):
"dbref matching only"
dbref = self.dbref(ostring)
if searchlist:
results = [prospect for prospect in searchlist
if prospect.id == dbref]
else:
results = self.filter(id=dbref)
return results
def run_full_search(ostring, searchlist, exact_match=False):
"full matching"
if searchlist:
results = match_list(searchlist, ostring,
exact_match, attribute_name)
elif attribute_name:
results = match_list(self.all(), ostring,
exact_match, attribute_name)
elif exact_match:
results = self.filter(db_key__iexact=ostring)
else:
results = self.filter(db_key__icontains=ostring)
return results
# Easiest case - dbref matching (always exact)
if self.dbref(ostring):
results = run_dbref_search(ostring)
if results:
return results
# Full search - this may return multiple matches.
results = run_full_search(ostring, searchlist, exact_match)
# Deal with results of full search
match_number = None
if not results:
# if we have no match, check if we are dealing
# with a "N-keyword" query, if so, strip it out.
match_number, ostring = IDPARSER(ostring)
if match_number != None and ostring:
# Run the search again, without the match number
results = run_full_search(ostring, searchlist, exact_match)
elif not exact_match:
# we have results, but are using fuzzy matching; run
# second sweep in results to catch eventual exact matches
# (these are given precedence, so a search for 'ball' in
# ['ball', 'ball2'] will correctly return the first ball
# only).
exact_results = run_full_search(ostring, results, True)
if exact_results:
results = exact_results
if len(results) > 1 and match_number != None:
# We have multiple matches, but a N-type match number
# is available to separate them.
try:
results = [results[match_number]]
except IndexError:
pass
# this is always a list.
return results
@returns_typeclass_list @returns_typeclass_list
def object_search(self, character, ostring, def object_search(self, character, ostring,
@ -248,59 +245,68 @@ class ObjectManager(TypedObjectManager):
Can be a dbref. If name is appended by *, a player is searched for. Can be a dbref. If name is appended by *, a player is searched for.
global_search: Search all objects, not just the current location/inventory global_search: Search all objects, not just the current location/inventory
attribute_name: (string) Which attribute to search in each object. attribute_name: (string) Which attribute to search in each object.
If None, the default 'name' attribute is used. If None, the default 'key' attribute is used.
""" """
ostring = str(ostring).strip() #ostring = str(ostring).strip()
if not ostring or not character: if not ostring or not character:
return None return None
location = character.location
# Easiest case - dbref matching (always exact)
dbref = self.dbref(ostring) dbref = self.dbref(ostring)
if dbref: if dbref:
# this is a valid dbref. If it matches, we return directly.
dbref_match = self.dbref_search(dbref) dbref_match = self.dbref_search(dbref)
if dbref_match: if dbref_match:
return [dbref_match] return [dbref_match]
# not a dbref. Search by attribute/property.
if not attribute_name:
# If the search string is one of the following, return immediately with
# the appropriate result.
if location and ostring == 'here':
return [location]
if character and ostring in ['me', 'self']:
return [character]
if character and ostring in ['*me', '*self']:
return [character.player]
location = character.location attribute_name = 'key'
# If the search string is one of the following, return immediately with if str(ostring).startswith("*"):
# the appropriate result. # Player search - try to find obj by its player's name
if location and ostring == 'here': player_string = ostring.lstrip("*")
return [location] player_match = self.get_obj_with_player(player_string)
if character and ostring in ['me', 'self']:
return [character]
if character and ostring in ['*me', '*self']:
return [character.player]
if ostring.startswith("*"):
# Player search - search player base
player_string = ostring.lstrip("*")
player_match = self.player_name_search(player_string)
if player_match is not None: if player_match is not None:
return [player_match] return [player_match]
# find suitable objects
if global_search or not location: if global_search or not location:
# search all objects # search all objects in database
return self.separable_search(ostring, None, objlist = self.get_objs_with_db_property(attribute_name)
attribute_name) if not objlist:
objlist = self.get_objs_with_attr(attribute_name)
# None of the above cases yielded a return, so we fall through to else:
# location/contents searches. # local search
matches = [] objlist = character.contents
local_objs = [] objlist.extend(location.contents)
local_objs.extend(character.contents) objlist.append(location) #easy to forget!
local_objs.extend(location.contents) if not objlist:
local_objs.append(location) #easy to forget! return []
if local_objs:
# normal key/attribute search (typedobject_search is # do the search on the found objects
# found in class parent) matches = separable_search(ostring, objlist,
matches = self.separable_search(ostring, local_objs, attribute_name, exact_match=False)
attribute_name, exact_match=False)
if not matches: if not matches and attribute_name in ('key', 'name'):
# no match, try an alias search # No matches. If we tried to match a key/name field, we also try to
matches = self.alias_list_search(ostring, local_objs) # see if an alias works better.
matches = self.alias_list_search(ostring, objlist)
return matches return matches
# #

View file

@ -418,8 +418,8 @@ class ObjectDB(TypedObject):
(if None, uses default 'name') (if None, uses default 'name')
use_nicks : Use nickname replace (off by default) use_nicks : Use nickname replace (off by default)
ignore_errors : Don't display any error messages even ignore_errors : Don't display any error messages even
if there are none/multiple matches - if there are none/multiple matches -
just return the result as a list. just return the result as a list.
Note - for multiple matches, the engine accepts a number Note - for multiple matches, the engine accepts a number
linked to the key in order to separate the matches from linked to the key in order to separate the matches from

View file

@ -90,6 +90,8 @@ def object_multimatch_parser(ostring):
that the engine assumes this number to start with 1 (i.e. not that the engine assumes this number to start with 1 (i.e. not
zero as in normal Python). zero as in normal Python).
""" """
if not isinstance(ostring, basestring):
return (None, ostring)
if not '-' in ostring: if not '-' in ostring:
return (None, ostring) return (None, ostring)
try: try:

View file

@ -24,6 +24,7 @@ class AttributeManager(models.Manager):
return self.filter(db_obj=obj).filter( return self.filter(db_obj=obj).filter(
db_key__icontains=searchstr) db_key__icontains=searchstr)
# #
# helper functions for the TypedObjectManager. # helper functions for the TypedObjectManager.
# #