diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 1a40241ef..07b45c1ae 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -71,7 +71,7 @@ CMD_CHANNEL = "__send_to_channel_command" CMD_LOGINSTART = "__unloggedin_look_command" # Function for handling multiple command matches. -_AT_MULTIMATCH_CMD = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_CMD.rsplit('.', 1)) +_SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) # Output strings @@ -488,7 +488,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess syscmd.matches = matches else: # fall back to default error handling - sysarg = yield _AT_MULTIMATCH_CMD(caller, matches) + sysarg = yield _SEARCH_AT_RESULT([match[2] for match in matches], caller, query=match[0]) raise ExecSystemCommand(syscmd, sysarg) if len(matches) == 1: diff --git a/evennia/commands/cmdparser.py b/evennia/commands/cmdparser.py index 168f70625..d9e8624dc 100644 --- a/evennia/commands/cmdparser.py +++ b/evennia/commands/cmdparser.py @@ -6,9 +6,13 @@ same inputs as the default one. """ -from django.utils.translation import ugettext as _ +import re +from django.conf import settings from evennia.utils.logger import log_trace +_MULTIMATCH_SEPARATOR = settings.SEARCH_MULTIMATCH_SEPARATOR +_MULTIMATCH_REGEX = re.compile(r"([0-9]+)%s(.*)" % _MULTIMATCH_SEPARATOR, re.I + re.U) + def cmdparser(raw_string, cmdset, caller, match_index=None): """ This function is called by the cmdhandler once it has @@ -83,16 +87,14 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): log_trace("cmdhandler error. raw_input:%s" % raw_string) if not matches: - # no matches found. - if '-' in raw_string: - # This could be due to the user trying to identify the - # command with a #num- style syntax. - mindex, new_raw_string = raw_string.split("-", 1) - if mindex.isdigit(): - mindex = int(mindex) - 1 - # feed result back to parser iteratively - return cmdparser(new_raw_string, cmdset, - caller, match_index=mindex) + # no matches found + num_ref_match = _MULTIMATCH_REGEX.match(raw_string) + if num_ref_match: + # the user might be trying to identify the command + # with a #num-command style syntax. + mindex, new_raw_string = num_ref_match.groups() + return cmdparser(new_raw_string, cmdset, + caller, match_index=int(mindex)) # only select command matches we are actually allowed to call. matches = [match for match in matches if match[2].access(caller, 'cmd')] @@ -127,161 +129,3 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): # no matter what we have at this point, we have to return it. return matches -#------------------------------------------------------------ -# Search parsers and support methods -#------------------------------------------------------------ -# -# Default functions for formatting and processing searches. -# -# You can replace these from the settings file by setting the variables -# -# SEARCH_AT_RESULT -# SEARCH_AT_MULTIMATCH_INPUT -# SEARCH_AT_MULTIMATCH_CMD -# -# The the replacing functions must have the same inputs and outputs as -# those in this module. -# -def at_search_result(msg_obj, ostring, results, global_search=False, - nofound_string=None, multimatch_string=None, quiet=False): - """ - Called by search methods after a result of any type has been found. - Takes a search result (a list) and formats eventual errors. - - Args: - msg_obj (Object): Object to receive feedback. - ostring (str): Original search string - results (list): List of found matches (0, 1 or more) - global_search (bool, optional): I this was a global_search or not (if it - is, there might be an idea of supplying dbrefs instead of only - numbers) - nofound_string (str, optional): Custom string for not-found error message. - multimatch_string (str, optional): Custom string for multimatch error header - quiet (bool, optional): Work normally, but don't echo to caller, just return the - results. - - Returns: - result (Object or None): The filtered object. If None, it suggests a - nofound/multimatch error and the error message was sent directly to `msg_obj`. If - the `multimatch_strin` was not given, the multimatch error will be returned as - - ``` - 1-object - 2-object - 3-object - etc - ``` - - """ - string = "" - if not results: - # no results. - if nofound_string: - # custom return string - string = nofound_string - else: - string = _("Could not find '%s'." % ostring) - results = None - - elif len(results) > 1: - # we have more than one match. We will display a - # list of the form 1-objname, 2-objname etc. - - if multimatch_string: - # custom header - string = multimatch_string - else: - string = "More than one match for '%s'" % ostring - string += " (please narrow target):" - string = _(string) - - for num, result in enumerate(results): - string += "\n %i-%s%s" % (num + 1, result.name, result.get_extra_info(msg_obj)) - results = None - else: - # we have exactly one match. - results = results[0] - - if string and not quiet: - msg_obj.msg(string.strip()) - return results - - -def at_multimatch_input(ostring): - """ - Parse number-identifiers. - - This parser will be called by the engine when a user supplies - a search term. The search term must be analyzed to determine - if the user wants to differentiate between multiple matches - (usually found during a previous search). - - Args: - ostring (str): The search criterion. The parser will specifically - understand input on a form like `2-object` to separate - multimatches from each other. - - Returns: - selection (tuple): This is on the form (index, ostring). - - Notes: - This method should separate out any identifiers from the search - string used to differentiate between same-named objects. The - result should be a tuple (index, search_string) where the index - gives which match among multiple matches should be used (1 being - the lowest number, rather than 0 as in Python). - - This will be parsed to (2, "object") and, if applicable, will tell - the engine to pick the second from a list of same-named matches of - objects called "object". - - Example: - > look - You see: ball, ball, ball and ball. - > get ball - There where multiple matches for ball: - 1-ball - 2-ball - 3-ball - 4-ball - > get 3-ball - You get the ball. - - """ - - if not isinstance(ostring, basestring): - return (None, ostring) - if not '-' in ostring: - return (None, ostring) - try: - index = ostring.find('-') - number = int(ostring[:index]) - 1 - return (number, ostring[index + 1:]) - except ValueError: - #not a number; this is not an identifier. - return (None, ostring) - except IndexError: - return (None, ostring) - - -def at_multimatch_cmd(caller, matches): - """ - Format multiple command matches to a useful error. - - Args: - caller (Object): Calling object. - matches (list): A list of matchtuples `(num, Command)`. - - Returns: - formatted (str): A nicely formatted string, including - eventual errors. - - """ - string = "There were multiple matches:" - for num, match in enumerate(matches): - # each match is a tuple (candidate, cmd) - cmdname, arg, cmd, dum, dum = match - - get_extra_info = cmd.get_extra_info(caller) - string += "\n %s-%s%s" % (num + 1, cmdname, get_extra_info) - return string diff --git a/evennia/commands/command.py b/evennia/commands/command.py index 20344699a..80f9ecd31 100644 --- a/evennia/commands/command.py +++ b/evennia/commands/command.py @@ -400,18 +400,22 @@ class Command(object): def get_extra_info(self, caller, **kwargs): """ - Display some extra information that may help distinguish this command from others, for instance, - in a disambiguity prompt. + Display some extra information that may help distinguish this + command from others, for instance, in a disambiguity prompt. - If this command is a potential match in an ambiguous situation, one distinguishing - feature may be its attachment to a nearby object, so we include this if available. + If this command is a potential match in an ambiguous + situation, one distinguishing feature may be its attachment to + a nearby object, so we include this if available. Args: - caller (TypedObject): The caller who typed an ambiguous term handed to the search function. + caller (TypedObject): The caller who typed an ambiguous + term handed to the search function. Returns: - A string with identifying information to disambiguate the object, conventionally with a preceding space. + A string with identifying information to disambiguate the + object, conventionally with a preceding space. + """ if hasattr(self, 'obj') and self.obj != caller: - return " (%s)" % self.obj.get_display_name(caller) + return " (%s)" % self.obj.get_display_name(caller).strip() return "" diff --git a/evennia/objects/manager.py b/evennia/objects/manager.py index fe85eb1aa..8504f3225 100644 --- a/evennia/objects/manager.py +++ b/evennia/objects/manager.py @@ -1,13 +1,13 @@ """ Custom manager for Objects. """ +import re from itertools import chain from django.db.models import Q from django.conf import settings from django.db.models.fields import exceptions from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager from evennia.typeclasses.managers import returns_typeclass, returns_typeclass_list -from evennia.utils import utils from evennia.utils.utils import to_unicode, is_iter, make_iter, string_partial_matching __all__ = ("ObjectManager",) @@ -16,12 +16,11 @@ _GA = object.__getattribute__ # delayed import _ATTR = None +_MULTIMATCH_REGEX = re.compile(r"([0-9]+)%s(.*)" % + settings.SEARCH_MULTIMATCH_SEPARATOR, re.I + re.U) # Try to use a custom way to parse id-tagged multimatches. -_AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) - - class ObjectDBManager(TypedObjectManager): """ This ObjectManager implements methods for searching @@ -379,9 +378,15 @@ class ObjectDBManager(TypedObjectManager): if not matches: # no matches found - check if we are dealing with N-keyword # query - if so, strip it. - match_number, searchdata = _AT_MULTIMATCH_INPUT(searchdata) - # run search again, with the exactness set by call + match = _MULTIMATCH_REGEX.match(searchdata) + match_number = None + if match: + # strips the number + match_number, searchdata = match.groups() + match_number = int(match_number) - 1 + match_number = match_number if match_number >= 0 else None if match_number is not None or not exact: + # run search again, with the exactness set by call matches = _searcher(searchdata, candidates, typeclass, exact=exact) # deal with result diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index bd746a2fb..ff3e8f628 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -334,7 +334,8 @@ class DefaultObject(ObjectDB): exact=exact) if quiet: return results - return _AT_SEARCH_RESULT(self, searchdata, results, global_search, nofound_string, multimatch_string) + return _AT_SEARCH_RESULT(results, self, query=searchdata, + nofound_string=nofound_string, multimatch_string=multimatch_string) def search_player(self, searchdata, quiet=False): """ diff --git a/evennia/players/players.py b/evennia/players/players.py index 7a79bf30e..78410d80e 100644 --- a/evennia/players/players.py +++ b/evennia/players/players.py @@ -458,7 +458,7 @@ class DefaultPlayer(PlayerDB): Notes: Extra keywords are ignored, but are allowed in call in order to make API more consistent with - objects.models.TypedObject.search. + objects.objects.DefaultObject.search. """ # handle me, self and *me, *self @@ -467,7 +467,7 @@ class DefaultPlayer(PlayerDB): if searchdata.lower() in ("me", "*me", "self", "*self",): return self matches = self.__class__.objects.player_search(searchdata) - matches = _AT_SEARCH_RESULT(self, searchdata, matches, global_search=True, + matches = _AT_SEARCH_RESULT(matches, self, query=searchdata, nofound_string=nofound_string, multimatch_string=multimatch_string) if matches and return_puppet: diff --git a/evennia/server/evennia_launcher.py b/evennia/server/evennia_launcher.py index 346e02bf1..303c7095f 100644 --- a/evennia/server/evennia_launcher.py +++ b/evennia/server/evennia_launcher.py @@ -699,8 +699,6 @@ def error_check_python_modules(): # core modules imp(settings.COMMAND_PARSER) imp(settings.SEARCH_AT_RESULT) - imp(settings.SEARCH_AT_MULTIMATCH_INPUT) - imp(settings.SEARCH_AT_MULTIMATCH_CMD) imp(settings.CONNECTION_SCREEN_MODULE) #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) for path in settings.LOCK_FUNC_MODULES: diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 51909d808..53bd257c9 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -236,16 +236,16 @@ CONN_MAX_AGE = 3600 * 7 # The command parser module to use. See the default module for which # functions it must implement COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser" -# The handler that outputs errors when searching -# objects using object.search(). -SEARCH_AT_RESULT = "evennia.commands.cmdparser.at_search_result" -# The parser used in order to separate multiple -# object matches (so you can separate between same-named -# objects without using dbrefs). -SEARCH_AT_MULTIMATCH_INPUT = "evennia.commands.cmdparser.at_multimatch_input" -# The parser used in order to separate multiple -# command matches (so you can separate between same-named commands) -SEARCH_AT_MULTIMATCH_CMD = "evennia.commands.cmdparser.at_multimatch_cmd" +# On a multi-match when search objects or commands, the user has the +# ability to search again with an index marker that differentiates +# the results. If multiple "box" objects are found, they can by +# default use 1-box, 2-box etc to refine the search. Below you +# can change the index separator character used. +SEARCH_MULTIMATCH_SEPARATOR = '-' +# The handler that outputs errors when using any API-level search +# (not manager methods). This function should correctly report errors +# both for command- and object-searches. +SEARCH_AT_RESULT = "evennia.utils.utils.at_search_result" # The module holding text strings for the connection screen. # This module should contain one or more variables # with strings defining the look of the screen. diff --git a/evennia/utils/search.py b/evennia/utils/search.py index 404d46e54..63ae49dbb 100644 --- a/evennia/utils/search.py +++ b/evennia/utils/search.py @@ -45,6 +45,11 @@ Channel = ContentType.objects.get(app_label="comms", model="channeldb").model_cl HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class() Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class() + +#------------------------------------------------------------------ +# Search manager-wrappers +#------------------------------------------------------------------ + # # Search objects as a character # diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 6ce5fb1b7..300993601 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -21,6 +21,9 @@ from collections import defaultdict from twisted.internet import threads, defer, reactor from django.conf import settings from django.utils import timezone +from django.utils.translation import ugettext as _ + +_MULTIMATCH_SEPARATOR = settings.SEARCH_MULTIMATCH_SEPARATOR try: import cPickle as pickle @@ -1226,3 +1229,60 @@ def m_len(target): if inherits_from(target, basestring): return len(ANSI_PARSER.strip_mxp(target)) return len(target) + +#------------------------------------------------------------------ +# Search handler function +#------------------------------------------------------------------ +# +# Replace this hook function by changing settings.SEARCH_AT_RESULT. +# + +def at_search_result(matches, caller, query="", quiet=False, **kwargs): + """ + This is a generic hook for handling all processing of a search + result, including error reporting. + + Args: + matches (list): This is a list of 0, 1 or more typeclass instances, + the matched result of the search. If 0, a nomatch error should + be echoed, and if >1, multimatch errors should be given. Only + if a single match should the result pass through. + caller (Object): The object performing the search and/or which should + receive error messages. + query (str, optional): The search query used to produce `matches`. + quiet (bool, optional): If `True`, no messages will be echoed to caller + on errors. + + Kwargs: + nofound_string (str): Replacement string to echo on a notfound error. + multimatch_string (str): Replacement string to echo on a multimatch error. + + Returns: + processed_result (Object or None): This is always a single result + or `None`. If `None`, any error reporting/handling should + already have happened. + """ + + error = "" + if not matches: + # no results. + error = kwargs.get("nofound_string", _("Could not find '%s'." % query)) + matches = None + elif len(matches) > 1: + error = kwargs.get("multimatch_string", None) + if not error: + error = _("More than one match for '%s'" \ + " (please narrow target):" % query) + for num, result in enumerate(matches): + error += "\n %i%s%s%s" % ( + num + 1, _MULTIMATCH_SEPARATOR, + result.get_display_name(caller) if hasattr(result, "get_display_name") else result.key, + result.get_extra_info(caller)) + matches = None + else: + # exactly one match + matches = matches[0] + + if error and not quiet: + caller.msg(error.strip()) + return matches