diff --git a/src/objects/manager.py b/src/objects/manager.py index a8b45902f6..fc3d1a08cc 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -1,17 +1,17 @@ """ Custom manager for Objects. """ +from django.db.models import Q from django.conf import settings #from django.contrib.auth.models import User from django.db.models.fields import exceptions from src.typeclasses.managers import TypedObjectManager from src.typeclasses.managers import returns_typeclass, returns_typeclass_list from src.utils import utils -from src.utils.utils import to_unicode, make_iter - -_ObjAttribute = None +from src.utils.utils import to_unicode, make_iter, string_partial_matching __all__ = ("ObjectManager",) +_GA = object.__getattribute__ # 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. @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. This search @@ -81,127 +81,138 @@ class ObjectManager(TypedObjectManager): the search criterion (e.g. in local_and_global_search). search_string: (string) The name or dbref to search for. """ - dbref = self.dbref(search_string) - if not dbref: - # not a dbref. Search by name. - search_string = to_unicode(search_string).lstrip('*') - return self.filter(db_player__user__username__iexact=search_string) - return self.get_id(dbref) + ostring = to_unicode(ostring).lstrip('*') + # simplest case - search by dbref + dbref = self.dbref(ostring) + if dbref: + return 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 - 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. """ - 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 @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 should be a valid location object. """ - global _ObjAttribute - if not _ObjAttribute: - 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] + cand_restriction = candidates and Q(objattribute__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() + return self.filter(cand_restriction & Q(objattribute__db_key=attribute_name)) @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 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 - if not _ObjAttribute: - from src.objects.models import ObjAttribute as _ObjAttribute - 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)] + cand_restriction = candidates and Q(db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() + attrs= self.model.objattribute_set.related.model.objects.select_related("db_obj").filter(cand_restriction & Q(db_key=attribute_name)) + return [attr.db_obj for attr in attrs if attribute_value == attr.value] @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. property_name = search string - location - actual location object to restrict to - + candidates - list of candidate objects to search """ - lstring = "" - if location: - lstring = ".filter(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: - return eval("self.exclude(db_%s=None)%s" % (property_name, lstring)) + return self.filter(cand_restriction).exclude(Q(property_name=None)) except exceptions.FieldError: return [] @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 """ - lstring = "" - if location: - lstring = ", db_location=location" - + if isinstance(property_value, basestring): + property_value = to_unicode(property_value) + 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: - if exact: - 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)) + return self.filter(cand_restriction & Q(property_name=property_value)) except exceptions.FieldError: 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 def get_contents(self, location, excludeobj=None): """ Get all objects that has a location 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) - for objkey in make_iter(excludeobj): - query = query.exclude(db_key=objkey) - return query + exclude_restriction = excludeobj and Q(pk__in=[_GA(obj, "in") for obj in make_iter(excludeobj)]) or Q() + return self.filter(db_location=location).exclude(exclude_restriction) + + @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 def object_search(self, ostring, caller=None, 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. 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. Can be a dbref. If name is appended by *, a player is searched for. 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 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 - of the callers. global_search will override this argument + candidates (list obj ObjectDBs): If objlist is supplied, global_search keyword is ignored + 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: 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) if not ostring and ostring != 0: @@ -237,81 +269,25 @@ class ObjectManager(TypedObjectManager): if 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_ 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. 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 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) - if match_number != None and ostring: - # Run search again, without match number: - 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) + # run search again, with the exactness set by caller + matches = _searcher(ostring, exact=exact) - # deal with the result + # deal with result 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: matches = [matches[match_number]] except IndexError: pass - # We always have a (possibly empty) list at this point. + # return a list (possibly empty) return matches # diff --git a/src/objects/models.py b/src/objects/models.py index 312e348dc8..9b0058856a 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -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.typeclass import TypeClass from src.objects.manager import ObjectManager -from src.players.models import PlayerDB from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler from src.scripts.scripthandler import ScriptHandler @@ -41,6 +40,11 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ +_ME = _("me") +_SELF = _("self") +_HERE = _("here") + + def clean_content_cache(obj): "Clean obj's content cache" _SA(obj, "_contents_cache", None) @@ -500,7 +504,8 @@ class ObjectDB(TypedObject): global_search=False, attribute_name=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 multiple results and lack thereof gracefully. @@ -509,17 +514,18 @@ class ObjectDB(TypedObject): Obs - To find a player, append * to the start of ostring. global_search: Search all objects, not just the current - location/inventory - attribute_name: (string) Which attribute to match - (if None, uses default 'name') + location/inventory. This is overruled if location keyword is given. + attribute_name: (string) Which attribute to match (if None, uses default 'name') use_nicks : Use nickname replace (off by default) 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 if there are none/multiple matches - just return the result as a list. - player : Don't search for an Object but a Player. - This will also find players that don't - currently have a character. + exact: Determines if the search must exactly match the key/alias of the + given object or if partial matches the beginnings of one or more + 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 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' 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 ostring.startswith('*') or player: # player nick replace @@ -549,21 +561,32 @@ class ObjectDB(TypedObject): # object nick replace ostring = self.nicks.get(ostring, nick_type="object") - if player: - if ostring in ("me", "self", "*me", "*self"): - results = [self.player] - else: - results = PlayerDB.objects.player_search(ostring.lstrip('*')) + candidates=None + if global_search: + # only allow exact matching if searching the entire database + exact = True else: - results = ObjectDB.objects.object_search(ostring, caller=self, - global_search=global_search, - attribute_name=attribute_name, - location=location) + # local search. Candidates are self.contents, self.location and self.location.contents + if not location: + location = self.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 diff --git a/src/objects/objects.py b/src/objects/objects.py index 51539d86e6..57903c2e0e 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -389,14 +389,14 @@ class Object(TypeClass): # controller, for example) dbref = self.dbobj.dbref - self.locks.add(";".join("control:perm(Immortals)", # edit locks/permissions, delete - "examine:perm(Builders)", # examine properties - "view:all()", # look at object (visibility) - "edit:perm(Wizards)", # edit properties/attributes - "delete:perm(Wizards)", # delete object - "get:all()", # pick up 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 + self.locks.add(";".join(["control:perm(Immortals)", # edit locks/permissions, delete + "examine:perm(Builders)", # examine properties + "view:all()", # look at object (visibility) + "edit:perm(Wizards)", # edit properties/attributes + "delete:perm(Wizards)", # delete object + "get:all()", # pick up 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 def basetype_posthook_setup(self): """ @@ -720,8 +720,8 @@ class Character(Object): overload the defaults (it is called after this one). """ super(Character, self).basetype_setup() - self.locks.add(";".join("get:false()", # noone can pick up the character - "call:false()")) # no commands can be called on character from outside + self.locks.add(";".join(["get:false()", # noone can pick up the character + "call:false()"])) # no commands can be called on character from outside # add the default cmdset from settings import CMDSET_DEFAULT self.cmdset.add_default(CMDSET_DEFAULT, permanent=True) @@ -788,8 +788,8 @@ class Room(Object): """ super(Room, self).basetype_setup() - self.locks.add(";".join("get:false()", - "puppet:false()")) # would be weird to puppet a room ... + self.locks.add(";".join(["get:false()", + "puppet:false()"])) # would be weird to puppet a room ... self.location = None # @@ -884,9 +884,9 @@ class Exit(Object): super(Exit, self).basetype_setup() # setting default locks (overload these in at_object_creation() - self.locks.add(";".join("puppet:false()", # would be weird to puppet an exit ... - "traverse:all()", # who can pass through exit by default - "get:false()")) # noone can pick up the exit + self.locks.add(";".join(["puppet:false()", # would be weird to puppet an exit ... + "traverse:all()", # who can pass through exit by default + "get:false()"])) # noone can pick up the exit # an exit should have a destination (this is replaced at creation time) if self.dbobj.location: diff --git a/src/scripts/manager.py b/src/scripts/manager.py index e22a2307aa..4625c09851 100644 --- a/src/scripts/manager.py +++ b/src/scripts/manager.py @@ -42,7 +42,6 @@ class ScriptManager(TypedObjectManager): if not obj: return [] if key: - script = [] dbref = self.dbref(key) if dbref: script = self.filter(db_obj=obj, id=dbref) diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index 2d9a2e6655..c478893e39 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -34,7 +34,7 @@ class AttributeManager(models.Manager): 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 return a list (may be empty). """ @@ -52,11 +52,11 @@ def returns_typeclass(method): def func(self, *args, **kwargs): "decorator. Returns result or None." self.__doc__ = method.__doc__ - rfunc = returns_typeclass_list(method) - try: - return rfunc(self, *args, **kwargs)[0] - except IndexError: - return None + matches = method(self, *args, **kwargs) + dbobj = matches and make_iter(matches)[0] or None + if dbobj: + return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj + return None return update_wrapper(func, method) diff --git a/src/utils/dummyrunner/dummyrunner_actions.py b/src/utils/dummyrunner/dummyrunner_actions.py index cd7521d9c2..f8416c5d77 100644 --- a/src/utils/dummyrunner/dummyrunner_actions.py +++ b/src/utils/dummyrunner/dummyrunner_actions.py @@ -66,7 +66,7 @@ def c_login(client): exitname1 = EXIT_TEMPLATE % client.counter() exitname2 = EXIT_TEMPLATE % client.counter() 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), 'connect %s %s' % (cname, cpwd), '@dig %s' % START_ROOM % client.cid, diff --git a/src/utils/utils.py b/src/utils/utils.py index 75cae91f35..7e4b10f06c 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -38,8 +38,7 @@ def is_iter(iterable): def make_iter(obj): "Makes sure that the object is always iterable." - if not hasattr(obj, '__iter__'): return [obj] - return obj + return not hasattr(obj, '__iter__') and [obj] or obj 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], 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 [] +