Resolve merge conflicts

This commit is contained in:
Griatch 2019-02-03 15:45:17 +01:00
commit e668160ab7
10 changed files with 241 additions and 150 deletions

View file

@ -15,6 +15,107 @@ _MULTIMATCH_REGEX = re.compile(settings.SEARCH_MULTIMATCH_REGEX, re.I + re.U)
_CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
def create_match(cmdname, string, cmdobj, raw_cmdname):
"""
Builds a command match by splitting the incoming string and
evaluating the quality of the match.
Args:
cmdname (str): Name of command to check for.
string (str): The string to match against.
cmdobj (str): The full Command instance.
raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
one of the prefixes to ignore, this contains the raw, unstripped cmdname,
otherwise it is None.
Returns:
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname),
where `cmdname` is the command's name and `args` is the rest of the incoming
string, without said command name. `cmdobj` is
the Command instance, the cmdlen is the same as len(cmdname) and mratio
is a measure of how big a part of the full input string the cmdname
takes up - an exact match would be 1.0. Finally, the `raw_cmdname` is
the cmdname unmodified by eventual prefix-stripping.
"""
cmdlen, strlen = len(str(cmdname)), len(str(string))
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
args = string[cmdlen:]
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
def build_matches(raw_string, cmdset, include_prefixes=False):
"""
Build match tuples by matching raw_string against available commands.
Args:
raw_string (str): Input string that can look in any way; the only assumption is
that the sought command's name/alias must be *first* in the string.
cmdset (CmdSet): The current cmdset to pick Commands from.
include_prefixes (bool): If set, include prefixes like @, ! etc (specified in settings)
in the match, otherwise strip them before matching.
Returns:
matches (list) A list of match tuples created by `cmdparser.create_match`.
"""
l_raw_string = raw_string.lower()
matches = []
try:
if include_prefixes:
# use the cmdname as-is
for cmd in cmdset:
matches.extend([create_match(cmdname, raw_string, cmd, cmdname)
for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower()) and
(not cmd.arg_regex or
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
else:
# strip prefixes set in settings
for cmd in cmdset:
for raw_cmdname in [cmd.key] + cmd.aliases:
cmdname = raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_cmdname) > 1 else raw_cmdname
if cmdname and l_raw_string.startswith(cmdname.lower()) and \
(not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):])):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
return matches
def try_num_prefixes(raw_string):
"""
Test if user tried to separate multi-matches with a number separator
(default 1-name, 2-name etc). This is usually called last, if no other
match was found.
Args:
raw_string (str): The user input to parse.
Returns:
mindex, new_raw_string (tuple): If a multimatch-separator was detected,
this is stripped out as an integer to separate between the matches. The
new_raw_string is the result of stripping out that identifier. If no
such form was found, returns (None, None).
Example:
In the default configuration, entering 2-ball (e.g. in a room will more
than one 'ball' object), will lead to a multimatch and this function
will parse `"2-ball"` and return `(2, "ball")`.
"""
# 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. We expect the regex to
# contain the groups "number" and "name".
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
return mindex, new_raw_string
else:
return None, None
def cmdparser(raw_string, cmdset, caller, match_index=None):
"""
This function is called by the cmdhandler once it has
@ -30,6 +131,10 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
the first run resulted in a multimatch, and the index is given
to select between the results for the second run.
Returns:
matches (list): This is a list of match-tuples as returned by `create_match`.
If no matches were found, this is an empty list.
Notes:
The cmdparser understand the following command combinations (where
[] marks optional parts.
@ -47,75 +152,11 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
the remaining arguments, and the matched cmdobject from the cmdset.
"""
def create_match(cmdname, string, cmdobj, raw_cmdname):
"""
Builds a command match by splitting the incoming string and
evaluating the quality of the match.
Args:
cmdname (str): Name of command to check for.
string (str): The string to match against.
cmdobj (str): The full Command instance.
raw_cmdname (str, optional): If CMD_IGNORE_PREFIX is set and the cmdname starts with
one of the prefixes to ignore, this contains the raw, unstripped cmdname,
otherwise it is None.
Returns:
match (tuple): This is on the form (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname), where
`cmdname` is the command's name and `args` the rest of the incoming string,
without said command name. `cmdobj` is the Command instance, the cmdlen is
the same as len(cmdname) and mratio is a measure of how big a part of the
full input string the cmdname takes up - an exact match would be 1.0. Finally,
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
"""
cmdlen, strlen = len(str(cmdname)), len(str(string))
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
args = string[cmdlen:]
return (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname)
def build_matches(raw_string, include_prefixes=False):
l_raw_string = raw_string.lower()
matches = []
try:
if include_prefixes:
# use the cmdname as-is
for cmd in cmdset:
matches.extend([create_match(cmdname, raw_string, cmd, cmdname)
for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower()) and
(not cmd.arg_regex or
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
else:
# strip prefixes set in settings
for cmd in cmdset:
for raw_cmdname in [cmd.key] + cmd.aliases:
cmdname = raw_cmdname.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_cmdname) > 1 else raw_cmdname
if cmdname and l_raw_string.startswith(cmdname.lower()) and \
(not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):])):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
return matches
def try_num_prefixes(raw_string):
if not matches:
# 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. We expect the regex to
# contain the groups "number" and "name".
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
return mindex, new_raw_string
return None, None
if not raw_string:
return []
# find mathces, first using the full name
matches = build_matches(raw_string, include_prefixes=True)
matches = build_matches(raw_string, cmdset, include_prefixes=True)
if not matches:
# try to match a number 1-cmdname, 2-cmdname etc
mindex, new_raw_string = try_num_prefixes(raw_string)
@ -124,7 +165,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if _CMD_IGNORE_PREFIXES:
# still no match. Try to strip prefixes
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
matches = build_matches(raw_string, include_prefixes=False)
matches = build_matches(raw_string, cmdset, include_prefixes=False)
# only select command matches we are actually allowed to call.
matches = [match for match in matches if match[2].access(caller, 'cmd')]

View file

@ -20,6 +20,7 @@ the line is just added to the editor buffer).
from evennia.comms.models import ChannelDB
from evennia.utils import create
from evennia.utils.utils import at_search_result
# The command keys the engine is calling
# (the actual names all start with __)
@ -76,57 +77,30 @@ class SystemMultimatch(COMMAND_DEFAULT_CLASS):
The cmdhandler adds a special attribute 'matches' to this
system command.
matches = [(candidate, cmd) , (candidate, cmd), ...],
matches = [(cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname) , (cmdname, ...), ...]
Here, `cmdname` is the command's name and `args` the rest of the incoming string,
without said command name. `cmdobj` is the Command instance, the cmdlen is
the same as len(cmdname) and mratio is a measure of how big a part of the
full input string the cmdname takes up - an exact match would be 1.0. Finally,
the `raw_cmdname` is the cmdname unmodified by eventual prefix-stripping.
where candidate is an instance of evennia.commands.cmdparser.CommandCandidate
and cmd is an an instantiated Command object matching the candidate.
"""
key = CMD_MULTIMATCH
locks = "cmd:all()"
def format_multimatches(self, caller, matches):
"""
Format multiple command matches to a useful error.
This is copied directly from the default method in
evennia.commands.cmdhandler.
"""
string = "There were multiple matches:"
for num, match in enumerate(matches):
# each match is a tuple (candidate, cmd)
candidate, cmd = match
is_channel = hasattr(cmd, "is_channel") and cmd.is_channel
if is_channel:
is_channel = " (channel)"
else:
is_channel = ""
is_exit = hasattr(cmd, "is_exit") and cmd.is_exit
if is_exit and cmd.destination:
is_exit = " (exit to %s)" % cmd.destination
else:
is_exit = ""
id1 = ""
id2 = ""
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller):
# the command is defined on some other object
id1 = "%s-" % cmd.obj.name
id2 = " (%s-%s)" % (num + 1, candidate.cmdname)
else:
id1 = "%s-" % (num + 1)
id2 = ""
string += "\n %s%s%s%s%s" % (id1, candidate.cmdname, id2, is_channel, is_exit)
return string
def func(self):
"""
argument to cmd is a comma-separated string of
all the clashing matches.
Handle multiple-matches by using the at_search_result default handler.
"""
string = self.format_multimatches(self.caller, self.matches)
self.msg(string)
# this was set by the cmdparser and is a tuple
# (cmdname, args, cmdobj, cmdlen, mratio, raw_cmdname). See
# evennia.commands.cmdparse.create_match for more details.
matches = self.matches
# at_search_result will itself msg the multimatch options to the caller.
at_search_result(
[match[2] for match in matches], self.caller, query=matches[0][0])
# Command called when the command given at the command line

View file

@ -21,9 +21,12 @@ from mock import Mock, mock
from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import EvenniaTest
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin, syscommands
from evennia.commands.cmdparser import build_matches
from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand
from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet
from evennia.utils import ansi, utils, gametime
from evennia.server.sessionhandler import SESSIONS
from evennia import search_object
@ -660,3 +663,33 @@ class TestUnconnectedCommand(CommandTest):
SESSIONS.account_count(), utils.get_evennia_version())
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
del gametime.SERVER_START_TIME
# Test syscommands
class TestSystemCommands(CommandTest):
def test_simple_defaults(self):
self.call(syscommands.SystemNoInput(), "")
self.call(syscommands.SystemNoMatch(), "Huh?")
def test_multimatch(self):
# set up fake matches and store on command instance
cmdset = CmdSet()
cmdset.add(general.CmdLook())
cmdset.add(general.CmdLook())
matches = cmdparser.build_matches("look", cmdset)
multimatch = syscommands.SystemMultimatch()
multimatch.matches = matches
self.call(multimatch, "look", "")
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
def test_channelcommand(self, mock_channeldb):
channel = mock.MagicMock()
channel.msg = mock.MagicMock()
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
self.call(syscommands.SystemSendToChannel(), "public:Hello")
channel.msg.assert_called()

View file

@ -165,7 +165,23 @@ def _get_prototype(inprot, protparents, uninherited=None, _workprot=None):
uninherited (dict): Parts of prototype to not inherit.
_workprot (dict, optional): Work dict for the recursive algorithm.
Returns:
merged (dict): A prototype where parent's have been merged as needed (the
`prototype_parent` key is removed).
"""
def _inherit_tags(old_tags, new_tags):
old = {(tup[0], tup[1]): tup for tup in old_tags}
new = {(tup[0], tup[1]): tup for tup in new_tags}
old.update(new)
return list(old.values())
def _inherit_attrs(old_attrs, new_attrs):
old = {(tup[0], tup[2]): tup for tup in old_attrs}
new = {(tup[0], tup[2]): tup for tup in new_attrs}
old.update(new)
return list(old.values())
_workprot = {} if _workprot is None else _workprot
if "prototype_parent" in inprot:
# move backwards through the inheritance
@ -173,8 +189,20 @@ def _get_prototype(inprot, protparents, uninherited=None, _workprot=None):
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(protparents.get(prototype.lower(), {}),
protparents, _workprot=_workprot)
# attrs, tags have internal structure that should be inherited separately
new_prot['attrs'] = _inherit_attrs(
_workprot.get("attrs", {}), new_prot.get("attrs", {}))
new_prot['tags'] = _inherit_tags(
_workprot.get("tags", {}), new_prot.get("tags", {}))
_workprot.update(new_prot)
# the inprot represents a higher level (a child prot), which should override parents
inprot['attrs'] = _inherit_attrs(
_workprot.get("attrs", {}), inprot.get("attrs", {}))
inprot['tags'] = _inherit_tags(
_workprot.get("tags", {}), inprot.get("tags", {}))
_workprot.update(inprot)
if uninherited:
# put back the parts that should not be inherited

View file

View file

@ -0,0 +1,19 @@
"""
Test initial startup procedure
"""
from mock import MagicMock, patch
from django.conf import settings
from django.test import TestCase
from evennia.server import initial_setup
class TestInitialSetup(TestCase):
@patch("evennia.server.initial_setup.AccountDB")
def test_get_god_account(self, mocked_accountdb):
mocked_accountdb.objects.get = MagicMock(return_value=1)
self.assertEqual(initial_setup.get_god_account(), 1)
mocked_accountdb.objects.get.assert_called_with(id=1)

View file

@ -1,28 +1,11 @@
# -*- coding: utf-8 -*-
"""
Unit testing of the 'objects' Evennia component.
Testing various individual functionalities in the server package.
Runs as part of the Evennia's test suite with 'manage.py test"
Please add new tests to this module as needed.
Guidelines:
A 'test case' is testing a specific component and is defined as a class
inheriting from unittest.TestCase. The test case class can have a method
setUp() that creates and sets up the testing environment.
All methods inside the test case class whose names start with 'test' are
used as test methods by the runner. Inside the test methods, special member
methods assert*() are used to test the behaviour.
"""
try:
from django.utils.unittest import TestCase
except ImportError:
from django.test import TestCase
try:
from django.utils import unittest
except ImportError:
import unittest
import unittest
from django.test import TestCase
from evennia.server.validators import EvenniaPasswordValidator
from evennia.utils.test_resources import EvenniaTest
@ -31,23 +14,7 @@ from django.test.runner import DiscoverRunner
from evennia.server.throttle import Throttle
from .deprecations import check_errors
class EvenniaTestSuiteRunner(DiscoverRunner):
"""
This test runner only runs tests on the apps specified in evennia/
avoid running the large number of tests defined by Django
"""
def build_suite(self, test_labels, extra_tests=None, **kwargs):
"""
Build a test suite for Evennia. test_labels is a list of apps to test.
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
import evennia
evennia._init()
return super().build_suite(test_labels, extra_tests=extra_tests, **kwargs)
from ..deprecations import check_errors
class MockSettings(object):

View file

@ -0,0 +1,27 @@
"""
Main test-suite runner of Evennia. The runner collates tests from
all over the code base and runs them.
Runs as part of the Evennia's test suite with 'evennia test evennia"
"""
from django.test.runner import DiscoverRunner
class EvenniaTestSuiteRunner(DiscoverRunner):
"""
Pointed to by the TEST_RUNNER setting.
This test runner only runs tests on the apps specified in evennia/
avoid running the large number of tests defined by Django
"""
def build_suite(self, test_labels, extra_tests=None, **kwargs):
"""
Build a test suite for Evennia. test_labels is a list of apps to test.
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
import evennia
evennia._init()
return super(EvenniaTestSuiteRunner, self).build_suite(
test_labels, extra_tests=extra_tests, **kwargs)

View file

@ -831,9 +831,9 @@ AUTH_USERNAME_VALIDATORS = [
{'NAME': 'evennia.server.validators.EvenniaUsernameAvailabilityValidator'}]
# Use a custom test runner that just tests Evennia-specific apps.
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
TEST_RUNNER = 'evennia.server.tests.testrunner.EvenniaTestSuiteRunner'
# Messages and Bootstrap don't classify events the same way; this setting maps
# Messages and Bootstrap don't classify events the same way; this setting maps
# messages.error() to Bootstrap 'danger' classes.
MESSAGE_TAGS = {
messages.ERROR: 'danger',

View file

@ -1853,7 +1853,9 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
Returns:
processed_result (Object or None): This is always a single result
or `None`. If `None`, any error reporting/handling should
already have happened.
already have happened. The returned object is of the type we are
checking multimatches for (e.g. Objects or Commands)
"""
error = ""