Changed how Objects are searched, using proper Django Q objects instead of hack-y evals to build queries. This has lead to a number of changes to the ObjectDB manager search. Notably there is now no way to supply a "location" to either of the manager search methods anymore. Instead you can now supply the keyword "candidates", a list of objects which should be used to limit the search. This is much more generic than giving location. The higher-level search (like caller.search, reached from commands) have not changed its API, so commands should work the same unless you are using the manager backbone directly. This search function is now using location to create the "candidates" list. Some other things, like matching for "me" and "here" have also been moved up to a level were it can be easily overloaded. "me" and "here" etc were also moved under i18n.

As part of this overhaul I implemented the partial_matching algorithm originally asked for by user "Adam_ASE" over IRC. This will allow for (local-only) partial matching of objects. So "big black sword" will now be matched by "bi", "sword", "bi bla" and so on. The partial matcher sits in src.utils.utils.py if one wants to use it for something else.
This commit is contained in:
Griatch 2012-09-17 15:31:50 +02:00
parent cc6fa079b6
commit c53a9b5770
7 changed files with 236 additions and 193 deletions

View file

@ -1,17 +1,17 @@
""" """
Custom manager for Objects. Custom manager for Objects.
""" """
from django.db.models import Q
from django.conf import settings from django.conf import settings
#from django.contrib.auth.models import User #from django.contrib.auth.models import User
from django.db.models.fields import exceptions from django.db.models.fields import exceptions
from src.typeclasses.managers import TypedObjectManager from src.typeclasses.managers import TypedObjectManager
from src.typeclasses.managers import returns_typeclass, returns_typeclass_list from src.typeclasses.managers import returns_typeclass, returns_typeclass_list
from src.utils import utils from src.utils import utils
from src.utils.utils import to_unicode, make_iter from src.utils.utils import to_unicode, make_iter, string_partial_matching
_ObjAttribute = None
__all__ = ("ObjectManager",) __all__ = ("ObjectManager",)
_GA = object.__getattribute__
# Try to use a custom way to parse id-tagged multimatches. # Try to use a custom way to parse id-tagged multimatches.
@ -73,7 +73,7 @@ class ObjectManager(TypedObjectManager):
# This returns typeclass since get_object_with_user and get_dbref does. # This returns typeclass since get_object_with_user and get_dbref does.
@returns_typeclass @returns_typeclass
def get_object_with_player(self, search_string): def get_object_with_player(self, ostring, exact=True, candidates=None):
""" """
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
@ -81,127 +81,138 @@ 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.
""" """
dbref = self.dbref(search_string) ostring = to_unicode(ostring).lstrip('*')
if not dbref: # simplest case - search by dbref
# not a dbref. Search by name. dbref = self.dbref(ostring)
search_string = to_unicode(search_string).lstrip('*') if dbref:
return self.filter(db_player__user__username__iexact=search_string) return dbref
return self.get_id(dbref) # not a dbref. Search by name.
cand_restriction = candidates and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
if exact:
return self.filter(cand_restriction & Q(db_player__user__username__iexact=ostring))
else: # fuzzy matching
ply_cands = self.filter(cand_restriction & Q(playerdb__user__username__istartswith=ostring)).values_list("db_key", flat=True)
if candidates:
index_matches = string_partial_matching(ply_cands, ostring, ret_index=True)
return [obj for ind, obj in enumerate(make_iter(candidates)) if ind in index_matches]
else:
return string_partial_matching(ply_cands, ostring, ret_index=False)
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_key_and_typeclass(self, oname, otypeclass_path): def get_objs_with_key_and_typeclass(self, oname, otypeclass_path, candidates=None):
""" """
Returns objects based on simultaneous key and typeclass match. Returns objects based on simultaneous key and typeclass match.
""" """
return self.filter(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path) cand_restriction = candidates and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
return self.filter(cand_restriction & Q(db_key__iexact=oname, db_typeclass_path__exact=otypeclass_path))
# attr/property related # attr/property related
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_attr(self, attribute_name, location=None): def get_objs_with_attr(self, attribute_name, candidates=None):
""" """
Returns all objects having the given attribute_name defined at all. Location Returns all objects having the given attribute_name defined at all. Location
should be a valid location object. should be a valid location object.
""" """
global _ObjAttribute cand_restriction = candidates and Q(objattribute__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
if not _ObjAttribute: return self.filter(cand_restriction & Q(objattribute__db_key=attribute_name))
from src.objects.models import ObjAttribute as _ObjAttribute
if location:
attrs = _ObjAttribute.objects.select_related("db_obj").filter(db_key=attribute_name, db_obj__db_location=location)
else:
attrs = _ObjAttribute.objects.select_related("db_obj").filter(db_key=attribute_name)
return [attr.db_obj for attr in attrs]
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_attr_match(self, attribute_name, attribute_value, location=None, exact=False): def get_objs_with_attr_value(self, attribute_name, attribute_value, candidates=None):
""" """
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. For this reason it does
not make sense to offer an "exact" type matching for this.
""" """
global _ObjAttribute cand_restriction = candidates and Q(db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
if not _ObjAttribute: attrs= self.model.objattribute_set.related.model.objects.select_related("db_obj").filter(cand_restriction & Q(db_key=attribute_name))
from src.objects.models import ObjAttribute as _ObjAttribute return [attr.db_obj for attr in attrs if attribute_value == attr.value]
if location:
attrs = _ObjAttribute.objects.select_related("db_value").filter(db_key=attribute_name, db_obj__db_location=location)
else:
attrs = _ObjAttribute.objects.select_related("db_value").filter(db_key=attribute_name)
if exact:
return [attr.obj for attr in attrs if attribute_value == attr.value]
else:
return [attr.obj for attr in attrs if to_unicode(attribute_value) in str(attr.value)]
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_db_property(self, property_name, location=None): def get_objs_with_db_property(self, property_name, candidates=None):
""" """
Returns all objects having a given db field property. Returns all objects having a given db field property.
property_name = search string property_name = search string
location - actual location object to restrict to candidates - list of candidate objects to search
""" """
lstring = "" property_name = "db_%s" % property_name.lstrip('db_')
if location: cand_restriction = candidates and Q(pk__in=[_GA(obj, "in") for obj in make_iter(candidates) if obj]) or Q()
lstring = ".filter(db_location=location)"
try: try:
return eval("self.exclude(db_%s=None)%s" % (property_name, lstring)) return self.filter(cand_restriction).exclude(Q(property_name=None))
except exceptions.FieldError: except exceptions.FieldError:
return [] return []
@returns_typeclass_list @returns_typeclass_list
def get_objs_with_db_property_match(self, property_name, property_value, location, exact=False): def get_objs_with_db_property_value(self, property_name, property_value, candidates=None):
""" """
Returns all objects having a given db field property Returns all objects having a given db field property
""" """
lstring = "" if isinstance(property_value, basestring):
if location: property_value = to_unicode(property_value)
lstring = ", db_location=location" property_name = "db_%s" % property_name.lstrip('db_')
cand_restriction = candidates and Q(pk__in=[_GA(obj, "in") for obj in make_iter(candidates) if obj]) or Q()
try: try:
if exact: return self.filter(cand_restriction & Q(property_name=property_value))
return eval("self.filter(db_%s__iexact=property_value%s)" % (property_name, lstring))
else:
return eval("self.filter(db_%s__icontains=property_value%s)" % (property_name, lstring))
except exceptions.FieldError: except exceptions.FieldError:
return [] return []
@returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, location=None, exact=False):
"""
Returns objects based on key or alias match
"""
lstring_key, lstring_alias, estring = "", "", "icontains"
if location:
lstring_key = ", db_location=location"
lstring_alias = ", db_obj__db_location=location"
if exact:
estring = "__iexact"
else:
estring = "__istartswith"
matches = eval("self.filter(db_key%s=ostring%s)" % (estring, lstring_key))
if not matches:
alias_matches = eval("self.model.alias_set.related.model.objects.filter(db_key%s=ostring%s)" % (estring, lstring_alias))
matches = [alias.db_obj for alias in alias_matches]
return matches
# 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):
""" """
Get all objects that has a location Get all objects that has a location
set to this one. set to this one.
excludeobjs - one or more object keys to exclude from the match excludeobj - one or more object keys to exclude from the match
""" """
query = self.filter(db_location__id=location.id) exclude_restriction = excludeobj and Q(pk__in=[_GA(obj, "in") for obj in make_iter(excludeobj)]) or Q()
for objkey in make_iter(excludeobj): return self.filter(db_location=location).exclude(exclude_restriction)
query = query.exclude(db_key=objkey)
return query @returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None):
"""
Returns objects based on key or alias match. Will also do fuzzy matching based on
the utils.string_partial_matching function.
"""
# build query objects
candidates_id = [_GA(obj, "id") for obj in make_iter(candidates) if obj]
cand_restriction = candidates and Q(pk__in=candidates_id) or Q()
if exact:
return self.filter(cand_restriction & (Q(db_key__iexact=ostring) | Q(alias__db_key__iexact=ostring)))
else:
if candidates:
# fuzzy matching - only check the candidates
key_candidates = self.filter(cand_restriction)
key_strings = key_candidates.values_list("db_key", flat=True)
index_matches = string_partial_matching(key_strings, ostring, ret_index=True)
if index_matches:
return [obj for ind, obj in enumerate(key_candidates) if ind in index_matches]
else:
alias_candidates = self.model.alias_set.related.model.objects.filter(db_obj__pk__in=candidates_id)
alias_strings = alias_candidates.values_list("db_key", flat=True)
index_matches = string_partial_matching(alias_strings, ostring, ret_index=True)
if index_matches:
return [alias.db_obj for ind, alias in enumerate(alias_candidates) if ind in index_matches]
return []
else:
# fuzzy matching - first check with keys, then with aliases
key_candidates = self.filter(Q(db_key__istartswith=ostring) | Q(alias__db_key__istartswith=ostring))
key_strings = key_candidates.values_list("db_key", flat=True)
matches = string_partial_matching(key_candidates, ostring, reg_index=False)
if matches:
return matches
alias_candidates = self.model.alias_set.related.model.objects.filter(db_obj__pk__in=candidates_id).values_list("db_key", flat=True)
return string_partial_matching(alias_candidates, ostring, ret_index=False)
# main search methods and helper functions
@returns_typeclass_list @returns_typeclass_list
def object_search(self, ostring, caller=None, def object_search(self, ostring, caller=None,
global_search=False, global_search=False,
attribute_name=None, location=None, single_result=False): attribute_name=None,
candidates=None,
exact=True):
""" """
Search as an object and return results. The result is always an Object. Search as an object and return results. The result is always an Object.
If * is appended (player search, a Character controlled by this Player If * is appended (player search, a Character controlled by this Player
@ -212,19 +223,40 @@ class ObjectManager(TypedObjectManager):
ostring: (string) The string to compare names against. ostring: (string) The string to compare names against.
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.
caller: (Object) The optional object performing the search. caller: (Object) The optional object performing the search.
global_search (bool). Defaults to False. If a caller is defined, search will
be restricted to the contents of caller.location unless global_search
is True. If no caller is given (or the caller has no location), a
global search is assumed automatically.
attribute_name: (string) Which object attribute to match ostring against. If not attribute_name: (string) Which object attribute to match ostring against. If not
set, the "key" and "aliases" properties are searched in order. set, the "key" and "aliases" properties are searched in order.
location (Object): If set, this location's contents will be used to limit the search instead candidates (list obj ObjectDBs): If objlist is supplied, global_search keyword is ignored
of the callers. global_search will override this argument and search will only be performed among the candidates in this list. A common list
of candidates is the contents of the current location searched.
exact (bool): Match names/aliases exactly or partially. Partial matching matches the
beginning of words in the names/aliases, using a matching routine to separate
multiple matches in names with multiple components (so "bi sw" will match
"Big sword"). Since this is more expensive than exact matching, it is
recommended to be used together with the objlist keyword to limit the number
of possibilities. This value has no meaning if searching for attributes/properties.
Returns: Returns:
A list of matching objects (or a list with one unique match) A list of matching objects (or a list with one unique match)
""" """
def _searcher(ostring, exact=False):
"Helper method for searching objects"
# Handle player
if ostring.startswith("*"):
# Player search - try to find obj by its player's name
player_match = self.get_object_with_player(ostring, candidates=candidates)
if player_match is not None:
return [player_match]
if attribute_name:
# attribute/property search (always exact).
matches = self.get_objs_with_db_property_value(attribute_name, ostring, candidates=candidates)
if not matches:
return self.get_objs_with_attr_value(attribute_name, ostring, candidates=candidates)
else:
# normal key/alias search
return self.get_objs_with_key_or_alias(ostring, exact=exact, candidates=candidates)
ostring = to_unicode(ostring, force_string=True) ostring = to_unicode(ostring, force_string=True)
if not ostring and ostring != 0: if not ostring and ostring != 0:
@ -237,81 +269,25 @@ class ObjectManager(TypedObjectManager):
if dbref_match: if dbref_match:
return [dbref_match] return [dbref_match]
if not location and caller and hasattr(caller, "location"):
location = caller.location
# Test some common self-references
if location and ostring == 'here':
return [location]
if caller and ostring in ('me', 'self'):
return [caller]
if caller and ostring in ('*me', '*self'):
return [caller]
# Test if we are looking for an object controlled by a
# specific player
#logger.log_infomsg(str(type(ostring)))
if ostring.startswith("*"):
# Player search - try to find obj by its player's name
player_match = self.get_object_with_player(ostring)
if player_match is not None:
return [player_match]
# Search for keys, aliases or other attributes
search_locations = [None] # this means a global search
if not global_search and location:
# Test if we are referring to the current room
if location and (ostring.lower() == location.key.lower()
or ostring.lower() in [alias.lower() for alias in location.aliases]):
return [location]
# otherwise, setup the locations to search in
search_locations = [location]
if caller:
search_locations.append(caller)
def local_and_global_search(ostring, exact=False):
"Helper method for searching objects"
matches = []
for location in search_locations:
if attribute_name:
# Attribute/property search. First, search for db_<attrname> matches on the model
matches.extend(self.get_objs_with_db_property_match(attribute_name, ostring, location, exact))
if not matches:
# Next, try Attribute matches
matches.extend(self.get_objs_with_attr_match(attribute_name, ostring, location, exact))
else:
# No attribute/property named. Do a normal key/alias-search
matches.extend(self.get_objs_with_key_or_alias(ostring, location, exact))
return matches
# Search through all possibilities. # Search through all possibilities.
match_number = None match_number = None
matches = local_and_global_search(ostring, exact=True) # always run first check exact - we don't want partial matches if on the form of 1-keyword etc.
matches = _searcher(ostring, exact=True)
if not matches: if not matches:
# if we have no match, check if we are dealing with an "N-keyword" query - if so, strip it. # no matches found - check if we are dealing with N-keyword query - if so, strip it.
match_number, ostring = _AT_MULTIMATCH_INPUT(ostring) match_number, ostring = _AT_MULTIMATCH_INPUT(ostring)
if match_number != None and ostring: # run search again, with the exactness set by caller
# Run search again, without match number: matches = _searcher(ostring, exact=exact)
matches = local_and_global_search(ostring, exact=True)
if ostring and (len(matches) > 1 or not matches):
# Already multimatch or no matches. Run a fuzzy matching.
matches = local_and_global_search(ostring, exact=False)
elif len(matches) > 1:
# multiple matches already. Run a fuzzy search. This catches partial matches (suggestions)
matches = local_and_global_search(ostring, exact=False)
# deal with the result # deal with result
if len(matches) > 1 and match_number != None: if len(matches) > 1 and match_number != None:
# We have multiple matches, but a N-type match number is available to separate them. # multiple matches, but a number was given to separate them
try: try:
matches = [matches[match_number]] matches = [matches[match_number]]
except IndexError: except IndexError:
pass pass
# We always have a (possibly empty) list at this point. # return a list (possibly empty)
return matches return matches
# #

View file

@ -23,7 +23,6 @@ from src.typeclasses.models import Attribute, TypedObject, TypeNick, TypeNickHan
from src.typeclasses.models import _get_cache, _set_cache, _del_cache from src.typeclasses.models import _get_cache, _set_cache, _del_cache
from src.typeclasses.typeclass import TypeClass from src.typeclasses.typeclass import TypeClass
from src.objects.manager import ObjectManager from src.objects.manager import ObjectManager
from src.players.models import PlayerDB
from src.commands.cmdsethandler import CmdSetHandler from src.commands.cmdsethandler import CmdSetHandler
from src.commands import cmdhandler from src.commands import cmdhandler
from src.scripts.scripthandler import ScriptHandler from src.scripts.scripthandler import ScriptHandler
@ -41,6 +40,11 @@ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
_DA = object.__delattr__ _DA = object.__delattr__
_ME = _("me")
_SELF = _("self")
_HERE = _("here")
def clean_content_cache(obj): def clean_content_cache(obj):
"Clean obj's content cache" "Clean obj's content cache"
_SA(obj, "_contents_cache", None) _SA(obj, "_contents_cache", None)
@ -500,7 +504,8 @@ class ObjectDB(TypedObject):
global_search=False, global_search=False,
attribute_name=None, attribute_name=None,
use_nicks=False, location=None, use_nicks=False, location=None,
ignore_errors=False, player=False): player=False,
ignore_errors=False, exact=False):
""" """
Perform a standard object search in the database, handling Perform a standard object search in the database, handling
multiple results and lack thereof gracefully. multiple results and lack thereof gracefully.
@ -509,17 +514,18 @@ class ObjectDB(TypedObject):
Obs - To find a player, append * to the Obs - To find a player, append * to the
start of ostring. start of ostring.
global_search: Search all objects, not just the current global_search: Search all objects, not just the current
location/inventory location/inventory. This is overruled if location keyword is given.
attribute_name: (string) Which attribute to match attribute_name: (string) Which attribute to match (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)
location : If None, use caller's current location location : If None, use caller's current location
player: return the Objects' controlling Player, instead, if available
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.
player : Don't search for an Object but a Player. exact: Determines if the search must exactly match the key/alias of the
This will also find players that don't given object or if partial matches the beginnings of one or more
currently have a character. words in the name is enough. Exact matching is faster if using
global search. Also, if attribute_name is set, matching is always exact.
Returns - a unique Object/Player match or None. All error Returns - a unique Object/Player match or None. All error
messages are handled by system-commands and the parser-handlers messages are handled by system-commands and the parser-handlers
@ -539,6 +545,12 @@ class ObjectDB(TypedObject):
address the individual ball as '1-ball', '2-ball', '3-ball' address the individual ball as '1-ball', '2-ball', '3-ball'
etc. etc.
""" """
# handle some common self-references:
if ostring == _HERE:
return self.location
if ostring in (_ME, _SELF, '*' + _ME, '*' + _SELF):
return self
if use_nicks: if use_nicks:
if ostring.startswith('*') or player: if ostring.startswith('*') or player:
# player nick replace # player nick replace
@ -549,21 +561,32 @@ class ObjectDB(TypedObject):
# object nick replace # object nick replace
ostring = self.nicks.get(ostring, nick_type="object") ostring = self.nicks.get(ostring, nick_type="object")
if player: candidates=None
if ostring in ("me", "self", "*me", "*self"): if global_search:
results = [self.player] # only allow exact matching if searching the entire database
else: exact = True
results = PlayerDB.objects.player_search(ostring.lstrip('*'))
else: else:
results = ObjectDB.objects.object_search(ostring, caller=self, # local search. Candidates are self.contents, self.location and self.location.contents
global_search=global_search, if not location:
attribute_name=attribute_name, location = self.location
location=location) candidates = self.contents
if location:
candidates = candidates + [location] + location.contents
else:
candidates.append(self) # normally we are included in location.contents
# db manager expects database objects
candidates = [obj.dbobj for obj in candidates]
results = ObjectDB.objects.object_search(ostring, caller=self,
attribute_name=attribute_name,
candidates=candidates,
exact=exact)
if not ignore_errors:
result = _AT_SEARCH_RESULT(self, ostring, results, global_search)
if player and result:
return result.player
return result
if ignore_errors:
return results
# this import is cache after the first call.
return _AT_SEARCH_RESULT(self, ostring, results, global_search)
# #
# Execution/action methods # Execution/action methods

View file

@ -389,14 +389,14 @@ class Object(TypeClass):
# controller, for example) # controller, for example)
dbref = self.dbobj.dbref dbref = self.dbobj.dbref
self.locks.add(";".join("control:perm(Immortals)", # edit locks/permissions, delete self.locks.add(";".join(["control:perm(Immortals)", # edit locks/permissions, delete
"examine:perm(Builders)", # examine properties "examine:perm(Builders)", # examine properties
"view:all()", # look at object (visibility) "view:all()", # look at object (visibility)
"edit:perm(Wizards)", # edit properties/attributes "edit:perm(Wizards)", # edit properties/attributes
"delete:perm(Wizards)", # delete object "delete:perm(Wizards)", # delete object
"get:all()", # pick up object "get:all()", # pick up object
"call:true()", # allow to call commands on this object "call:true()", # allow to call commands on this object
"puppet:id(%s) or perm(Immortals) or pperm(Immortals)" % dbref)) # restricts puppeting of this object "puppet:id(%s) or perm(Immortals) or pperm(Immortals)" % dbref])) # restricts puppeting of this object
def basetype_posthook_setup(self): def basetype_posthook_setup(self):
""" """
@ -720,8 +720,8 @@ class Character(Object):
overload the defaults (it is called after this one). overload the defaults (it is called after this one).
""" """
super(Character, self).basetype_setup() super(Character, self).basetype_setup()
self.locks.add(";".join("get:false()", # noone can pick up the character self.locks.add(";".join(["get:false()", # noone can pick up the character
"call:false()")) # no commands can be called on character from outside "call:false()"])) # no commands can be called on character from outside
# add the default cmdset # add the default cmdset
from settings import CMDSET_DEFAULT from settings import CMDSET_DEFAULT
self.cmdset.add_default(CMDSET_DEFAULT, permanent=True) self.cmdset.add_default(CMDSET_DEFAULT, permanent=True)
@ -788,8 +788,8 @@ class Room(Object):
""" """
super(Room, self).basetype_setup() super(Room, self).basetype_setup()
self.locks.add(";".join("get:false()", self.locks.add(";".join(["get:false()",
"puppet:false()")) # would be weird to puppet a room ... "puppet:false()"])) # would be weird to puppet a room ...
self.location = None self.location = None
# #
@ -884,9 +884,9 @@ class Exit(Object):
super(Exit, self).basetype_setup() super(Exit, self).basetype_setup()
# setting default locks (overload these in at_object_creation() # setting default locks (overload these in at_object_creation()
self.locks.add(";".join("puppet:false()", # would be weird to puppet an exit ... self.locks.add(";".join(["puppet:false()", # would be weird to puppet an exit ...
"traverse:all()", # who can pass through exit by default "traverse:all()", # who can pass through exit by default
"get:false()")) # noone can pick up the exit "get:false()"])) # noone can pick up the exit
# an exit should have a destination (this is replaced at creation time) # an exit should have a destination (this is replaced at creation time)
if self.dbobj.location: if self.dbobj.location:

View file

@ -42,7 +42,6 @@ class ScriptManager(TypedObjectManager):
if not obj: if not obj:
return [] return []
if key: if key:
script = []
dbref = self.dbref(key) dbref = self.dbref(key)
if dbref: if dbref:
script = self.filter(db_obj=obj, id=dbref) script = self.filter(db_obj=obj, id=dbref)

View file

@ -34,7 +34,7 @@ class AttributeManager(models.Manager):
def returns_typeclass_list(method): def returns_typeclass_list(method):
""" """
Decorator: Chantes return of the decorated method (which are Decorator: Changes return of the decorated method (which are
TypeClassed objects) into object_classes(s) instead. Will always TypeClassed objects) into object_classes(s) instead. Will always
return a list (may be empty). return a list (may be empty).
""" """
@ -52,11 +52,11 @@ def returns_typeclass(method):
def func(self, *args, **kwargs): def func(self, *args, **kwargs):
"decorator. Returns result or None." "decorator. Returns result or None."
self.__doc__ = method.__doc__ self.__doc__ = method.__doc__
rfunc = returns_typeclass_list(method) matches = method(self, *args, **kwargs)
try: dbobj = matches and make_iter(matches)[0] or None
return rfunc(self, *args, **kwargs)[0] if dbobj:
except IndexError: return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
return None return None
return update_wrapper(func, method) return update_wrapper(func, method)

View file

@ -66,7 +66,7 @@ def c_login(client):
exitname1 = EXIT_TEMPLATE % client.counter() exitname1 = EXIT_TEMPLATE % client.counter()
exitname2 = EXIT_TEMPLATE % client.counter() exitname2 = EXIT_TEMPLATE % client.counter()
client.exits.extend([exitname1, exitname2]) client.exits.extend([exitname1, exitname2])
cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2) #cmd = '@dig %s = %s, %s' % (roomname, exitname1, exitname2)
cmd = ('create %s %s' % (cname, cpwd), cmd = ('create %s %s' % (cname, cpwd),
'connect %s %s' % (cname, cpwd), 'connect %s %s' % (cname, cpwd),
'@dig %s' % START_ROOM % client.cid, '@dig %s' % START_ROOM % client.cid,

View file

@ -38,8 +38,7 @@ def is_iter(iterable):
def make_iter(obj): def make_iter(obj):
"Makes sure that the object is always iterable." "Makes sure that the object is always iterable."
if not hasattr(obj, '__iter__'): return [obj] return not hasattr(obj, '__iter__') and [obj] or obj
return obj
def fill(text, width=78, indent=0): def fill(text, width=78, indent=0):
""" """
@ -976,3 +975,49 @@ def string_suggestions(string, vocabulary, cutoff=0.6, maxnum=3):
""" """
return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg) for sugg in vocabulary], return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg) for sugg in vocabulary],
key=lambda tup: tup[0], reverse=True) if tup[0] >= cutoff][:maxnum] key=lambda tup: tup[0], reverse=True) if tup[0] >= cutoff][:maxnum]
def string_partial_matching(alternatives, inp, ret_index=True):
"""
Partially matches a string based on a list of alternatives. Matching
is made from the start of each subword in each alternative. Case is not
important. So e.g. "bi sh sw" or just "big" or "shiny" or "sw" will match
"Big shiny sword". Scoring is done to allow to separate by most common
demoninator. You will get multiple matches returned if appropriate.
Input:
alternatives (list of str) - list of possible strings to match
inp (str) - search criterion
ret_index (bool) - return list of indices (from alternatives array) or strings
Returns:
list of matching indices or strings, or an empty list
"""
if not alternatives or not inp:
return []
matches = defaultdict(list)
inp_words = inp.lower().split()
for altindex, alt in enumerate(alternatives):
alt_words = alt.lower().split()
last_index = 0
score = 0
for inp_word in inp_words:
# loop over parts, making sure only to visit each part once
# (this will invalidate input in the wrong word order)
submatch = [last_index + alt_num for alt_num, alt_word
in enumerate(alt_words[last_index:]) if alt_word.startswith(inp_word)]
if submatch:
last_index = min(submatch) + 1
score += 1
else:
score = 0
break
if score:
if ret_index:
matches[score].append(altindex)
else:
matches[score].append(alt)
if matches:
return matches[max(matches)]
return []