Removed SEARCH_AT_MULTIMATCH_INPUT and SEARCH_AT_MULTIMATCH_CMD settings and connected functions - these are no longer individually overloadable. SEARCH_AT_RESULT function now handles all error reporting. Also added SEARCH_MULTIMATCH_SEPARATOR to make it easy to replace the character used to separate multi-matches (1-box, 2-box is using '-' by default), in response to #795. Also moved the default SEARCH_AT_RESULT function from the cmdparser to evennia.utils.utils.

This commit is contained in:
Griatch 2015-09-27 13:05:29 +02:00
parent 5429ede5f7
commit 2743f98fb0
10 changed files with 116 additions and 199 deletions

View file

@ -71,7 +71,7 @@ CMD_CHANNEL = "__send_to_channel_command"
CMD_LOGINSTART = "__unloggedin_look_command" CMD_LOGINSTART = "__unloggedin_look_command"
# Function for handling multiple command matches. # 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 # Output strings
@ -488,7 +488,7 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
syscmd.matches = matches syscmd.matches = matches
else: else:
# fall back to default error handling # 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) raise ExecSystemCommand(syscmd, sysarg)
if len(matches) == 1: if len(matches) == 1:

View file

@ -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 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): def cmdparser(raw_string, cmdset, caller, match_index=None):
""" """
This function is called by the cmdhandler once it has 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) log_trace("cmdhandler error. raw_input:%s" % raw_string)
if not matches: if not matches:
# no matches found. # no matches found
if '-' in raw_string: num_ref_match = _MULTIMATCH_REGEX.match(raw_string)
# This could be due to the user trying to identify the if num_ref_match:
# command with a #num-<command> style syntax. # the user might be trying to identify the command
mindex, new_raw_string = raw_string.split("-", 1) # with a #num-command style syntax.
if mindex.isdigit(): mindex, new_raw_string = num_ref_match.groups()
mindex = int(mindex) - 1 return cmdparser(new_raw_string, cmdset,
# feed result back to parser iteratively caller, match_index=int(mindex))
return cmdparser(new_raw_string, cmdset,
caller, match_index=mindex)
# only select command matches we are actually allowed to call. # only select command matches we are actually allowed to call.
matches = [match for match in matches if match[2].access(caller, 'cmd')] 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. # no matter what we have at this point, we have to return it.
return matches 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

View file

@ -400,18 +400,22 @@ class Command(object):
def get_extra_info(self, caller, **kwargs): def get_extra_info(self, caller, **kwargs):
""" """
Display some extra information that may help distinguish this command from others, for instance, Display some extra information that may help distinguish this
in a disambiguity prompt. command from others, for instance, in a disambiguity prompt.
If this command is a potential match in an ambiguous situation, one distinguishing If this command is a potential match in an ambiguous
feature may be its attachment to a nearby object, so we include this if available. situation, one distinguishing feature may be its attachment to
a nearby object, so we include this if available.
Args: 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: 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: 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 "" return ""

View file

@ -1,13 +1,13 @@
""" """
Custom manager for Objects. Custom manager for Objects.
""" """
import re
from itertools import chain from itertools import chain
from django.db.models import Q from django.db.models import Q
from django.conf import settings from django.conf import settings
from django.db.models.fields import exceptions from django.db.models.fields import exceptions
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
from evennia.typeclasses.managers import returns_typeclass, returns_typeclass_list 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 from evennia.utils.utils import to_unicode, is_iter, make_iter, string_partial_matching
__all__ = ("ObjectManager",) __all__ = ("ObjectManager",)
@ -16,12 +16,11 @@ _GA = object.__getattribute__
# delayed import # delayed import
_ATTR = None _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. # 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): class ObjectDBManager(TypedObjectManager):
""" """
This ObjectManager implements methods for searching This ObjectManager implements methods for searching
@ -379,9 +378,15 @@ class ObjectDBManager(TypedObjectManager):
if not matches: if not matches:
# no matches found - check if we are dealing with N-keyword # no matches found - check if we are dealing with N-keyword
# query - if so, strip it. # query - if so, strip it.
match_number, searchdata = _AT_MULTIMATCH_INPUT(searchdata) match = _MULTIMATCH_REGEX.match(searchdata)
# run search again, with the exactness set by call 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: 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) matches = _searcher(searchdata, candidates, typeclass, exact=exact)
# deal with result # deal with result

View file

@ -334,7 +334,8 @@ class DefaultObject(ObjectDB):
exact=exact) exact=exact)
if quiet: if quiet:
return results 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): def search_player(self, searchdata, quiet=False):
""" """

View file

@ -458,7 +458,7 @@ class DefaultPlayer(PlayerDB):
Notes: Notes:
Extra keywords are ignored, but are allowed in call in Extra keywords are ignored, but are allowed in call in
order to make API more consistent with order to make API more consistent with
objects.models.TypedObject.search. objects.objects.DefaultObject.search.
""" """
# handle me, self and *me, *self # handle me, self and *me, *self
@ -467,7 +467,7 @@ class DefaultPlayer(PlayerDB):
if searchdata.lower() in ("me", "*me", "self", "*self",): if searchdata.lower() in ("me", "*me", "self", "*self",):
return self return self
matches = self.__class__.objects.player_search(searchdata) 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, nofound_string=nofound_string,
multimatch_string=multimatch_string) multimatch_string=multimatch_string)
if matches and return_puppet: if matches and return_puppet:

View file

@ -699,8 +699,6 @@ def error_check_python_modules():
# core modules # core modules
imp(settings.COMMAND_PARSER) imp(settings.COMMAND_PARSER)
imp(settings.SEARCH_AT_RESULT) imp(settings.SEARCH_AT_RESULT)
imp(settings.SEARCH_AT_MULTIMATCH_INPUT)
imp(settings.SEARCH_AT_MULTIMATCH_CMD)
imp(settings.CONNECTION_SCREEN_MODULE) imp(settings.CONNECTION_SCREEN_MODULE)
#imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
for path in settings.LOCK_FUNC_MODULES: for path in settings.LOCK_FUNC_MODULES:

View file

@ -236,16 +236,16 @@ CONN_MAX_AGE = 3600 * 7
# The command parser module to use. See the default module for which # The command parser module to use. See the default module for which
# functions it must implement # functions it must implement
COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser" COMMAND_PARSER = "evennia.commands.cmdparser.cmdparser"
# The handler that outputs errors when searching # On a multi-match when search objects or commands, the user has the
# objects using object.search(). # ability to search again with an index marker that differentiates
SEARCH_AT_RESULT = "evennia.commands.cmdparser.at_search_result" # the results. If multiple "box" objects are found, they can by
# The parser used in order to separate multiple # default use 1-box, 2-box etc to refine the search. Below you
# object matches (so you can separate between same-named # can change the index separator character used.
# objects without using dbrefs). SEARCH_MULTIMATCH_SEPARATOR = '-'
SEARCH_AT_MULTIMATCH_INPUT = "evennia.commands.cmdparser.at_multimatch_input" # The handler that outputs errors when using any API-level search
# The parser used in order to separate multiple # (not manager methods). This function should correctly report errors
# command matches (so you can separate between same-named commands) # both for command- and object-searches.
SEARCH_AT_MULTIMATCH_CMD = "evennia.commands.cmdparser.at_multimatch_cmd" SEARCH_AT_RESULT = "evennia.utils.utils.at_search_result"
# The module holding text strings for the connection screen. # The module holding text strings for the connection screen.
# This module should contain one or more variables # This module should contain one or more variables
# with strings defining the look of the screen. # with strings defining the look of the screen.

View file

@ -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() HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class()
Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class() Tag = ContentType.objects.get(app_label="typeclasses", model="tag").model_class()
#------------------------------------------------------------------
# Search manager-wrappers
#------------------------------------------------------------------
# #
# Search objects as a character # Search objects as a character
# #

View file

@ -21,6 +21,9 @@ from collections import defaultdict
from twisted.internet import threads, defer, reactor from twisted.internet import threads, defer, reactor
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _
_MULTIMATCH_SEPARATOR = settings.SEARCH_MULTIMATCH_SEPARATOR
try: try:
import cPickle as pickle import cPickle as pickle
@ -1226,3 +1229,60 @@ def m_len(target):
if inherits_from(target, basestring): if inherits_from(target, basestring):
return len(ANSI_PARSER.strip_mxp(target)) return len(ANSI_PARSER.strip_mxp(target))
return len(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