diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 24a92cc7f6..49b397e452 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -55,8 +55,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): Talk on and manage in-game channels. Usage: - channel channelname [= ] channel + channel channelname [= ] channel/list channel/all channel/history channelname [= index] @@ -88,7 +88,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): "create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",) # note - changing this will invalidate existing aliases in db - channel_msg_nick_alias = "{alias} $1" + # channel_msg_nick_alias = r"{alias}\s*?(?P.+?){{0,1}}" + channel_msg_nick_alias = r"{alias} (?P.+?)" channel_msg_nick_replacement = "channel {channelname} = $1" def search_channel(self, channelname, exact=False): @@ -240,11 +241,13 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): """ chan_key = channel.key.lower() msg_alias = self.channel_msg_nick_alias.format(alias=alias) + print("msg_alias", msg_alias) msg_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key) if chan_key != alias: self.caller.nicks.add(alias, chan_key, category="channel", **kwargs) - self.caller.nicks.add(msg_alias, msg_replacement, category="channel", **kwargs) + self.caller.nicks.add(msg_alias, msg_replacement, category="inputline", + regex_pattern=True, **kwargs) def remove_alias(self, alias, **kwargs): """ @@ -269,8 +272,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): if caller.nicks.get(alias, category="channel", **kwargs): msg_alias = self.channel_msg_nick_alias.format(alias=alias) caller.nicks.remove(alias, category="channel", **kwargs) - caller.nicks.remove(msg_alias, category="channel", **kwargs) + caller.nicks.remove(msg_alias, category="inputline", **kwargs) return True, "" + return False, "No such alias was defined." def get_channel_aliases(self, channel): @@ -724,7 +728,6 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): self.msg(err) return - channels = [] for channel_name in channel_names: # find a channel by fuzzy-matching. This also checks diff --git a/evennia/server/inputfuncs.py b/evennia/server/inputfuncs.py index bf825f1b8b..cb0af336d6 100644 --- a/evennia/server/inputfuncs.py +++ b/evennia/server/inputfuncs.py @@ -78,11 +78,11 @@ def text(session, *args, **kwargs): puppet = session.puppet if puppet: txt = puppet.nicks.nickreplace( - txt, categories=("inputline", "channel"), include_account=True + txt, categories=("inputline"), include_account=True ) else: txt = session.account.nicks.nickreplace( - txt, categories=("inputline", "channel"), include_account=False + txt, categories=("inputline"), include_account=False ) kwargs.pop("options", None) cmdhandler(session, txt, callertype="session", session=session, **kwargs) diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 74c2f7f32a..e68586e2e5 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -1291,6 +1291,7 @@ Custom arg markers $N argument position (1-99) """ +_RE_NICK_RE_ARG = re.compile(r"arg([1-9][0-9]?)") _RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") _RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)") _RE_NICK_SPACE = re.compile(r"\\ ") @@ -1300,45 +1301,59 @@ class NickTemplateInvalid(ValueError): pass -def initialize_nick_templates(in_template, out_template): +def initialize_nick_templates(pattern, out_template, pattern_is_regex=False): """ Initialize the nick templates for matching and remapping a string. Args: - in_template (str): The template to be used for nick recognition. + pattern (str): The pattern to be used for nick recognition. This will + be parsed for shell patterns into a regex, unless `pattern_is_regex` + is `True`, in which case it must be an already valid regex string. In + this case, instead of `$N`, numbered arguments must instead be given + as matching groups named as `argN`, such as `(?P.+?)`. Such + groups can then also be made optional using e.g. `{?P.*?}`. out_template (str): The template to be used to replace the string - matched by the in_template. + matched by the pattern. This can contain `$N` markers and is never + parsed into a regex. Returns: - regex (regex): Regex to match against strings - template (str): Template with markers ``{arg1}, {arg2}``, etc for - replacement using the standard .format method. + regex, template (str): Regex to match against strings and template + with markers ``{arg1}, {arg2}``, etc for replacement using the standard + `.format` method. Raises: evennia.typecalasses.attributes.NickTemplateInvalid: If the in/out template does not have a matching number of `$args`. + Example: + in->out-template: `grin $1 -> emote gives a wicked grin to $1.` + """ - # create the regex for in_template - regex_string = fnmatch.translate(in_template) + # create the regex from the pattern + if pattern_is_regex: + # Explicit regex given from the onset - this already contains argN groups + pattern_regex_string = pattern + r"\Z" + else: + # regex generated by parsing shell pattern syntax - convert $N to argN groups + pattern_regex_string = fnmatch.translate(pattern) + pattern_regex_string = _RE_NICK_SPACE.sub(r"\\s+", pattern_regex_string) + pattern_regex_string = _RE_NICK_ARG.sub( + lambda m: "(?P.+?)" % m.group(2), pattern_regex_string) # we must account for a possible line break coming over the wire + pattern_regex_string = pattern_regex_string[:-2] + r"(?:[\n\r]*?)\Z" - # NOTE-PYTHON3: fnmatch.translate format changed since Python2 - regex_string = regex_string[:-2] + r"(?:[\n\r]*?)\Z" + # count and compare the args-nums in pattern and replacement for validation + pattern_args = [match.group(1) for match in _RE_NICK_RE_ARG.finditer(pattern_regex_string)] + replacement_args = [match.group(2) for match in _RE_NICK_TEMPLATE_ARG.finditer(out_template)] + if set(pattern_args) != set(replacement_args): + # We don't have the same amount of argN/$N tags in input/output. + raise NickTemplateInvalid("Nicks: Both in/out-templates must contain the same $N tags.") - # validate the templates - regex_args = [match.group(2) for match in _RE_NICK_ARG.finditer(regex_string)] - temp_args = [match.group(2) for match in _RE_NICK_TEMPLATE_ARG.finditer(out_template)] - if set(regex_args) != set(temp_args): - # We don't have the same $-tags in input/output. - raise NickTemplateInvalid - - regex_string = _RE_NICK_SPACE.sub(r"\\s+", regex_string) - regex_string = _RE_NICK_ARG.sub(lambda m: "(?P.+?)" % m.group(2), regex_string) + # map the replacement to match the arg1 group-names, to make replacement easy template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template) - return regex_string, template_string + return pattern_regex_string, template_string def parse_nick_template(string, template_regex, outtemplate): @@ -1346,7 +1361,7 @@ def parse_nick_template(string, template_regex, outtemplate): Parse a text using a template and map it to another template Args: - string (str): The input string to processj + string (str): The input string to process template_regex (regex): A template regex created with initialize_nick_template. outtemplate (str): The template to which to map the matches @@ -1408,8 +1423,6 @@ class NickHandler(AttributeHandler): Returns: str or tuple: The nick replacement string or nick tuple. - - """ if return_tuple or "return_obj" in kwargs: return super().get(key=key, category=category, **kwargs) @@ -1423,24 +1436,46 @@ class NickHandler(AttributeHandler): ) return None - def add(self, key, replacement, category="inputline", **kwargs): + def add(self, pattern, replacement, category="inputline", pattern_is_regex=False, **kwargs): """ - Add a new nick. + Add a new nick, a mapping pattern -> replacement. Args: - key (str): A key (or template) for the nick to match for. - replacement (str): The string (or template) to replace `key` with (the "nickname"). + pattern (str): A pattern to match for. This will be parsed for + shell patterns using the `fnmatch` library and can contain + `$N`-markers to indicate the locations of arguments to catch. If + `pattern_is_regex=True`, this must instead be a valid regular + expression and the `$N`-markers must be named `argN` matching + groups (see examples). + replacement (str): The string (or template) to replace `key` with + (the "nickname"). This may contain `$N` markers to indicate where to + place the argument-matches category (str, optional): the category within which to retrieve the nick. The "inputline" means replacing data sent by the user. + pattern_is_regex (bool): If `True`, the `pattern` will be parsed as a + raw regex string. Instead of using `$N` markers in this string, one + then must mark numbered arguments as a named regex-groupd named `argN`. + For example, `(?P.+?)` will match the behavior of using `$1` + in the shell pattern. kwargs (any, optional): These are passed on to `AttributeHandler.get`. + Notes: + For most cases, the shell-pattern is much shorter and easier. The + regex pattern form can be useful for more complex matchings though, + for example in order to add optional arguments, such as with + `(?P.*?)`. + + Example: + - pattern (default shell syntax): `"gr $1 at $2"` + - pattern (with pattern_is_regex=True): `r"gr (?P.+?) at (?P.+?)" + - replacement: `"emote With a flourish, $1 grins at $2."` + """ - # if category == "channel": - # nick_regex, nick_template = initialize_nick_templates(key + " $1", replacement + " $1") - # else: - nick_regex, nick_template = initialize_nick_templates(key, replacement) - super().add(key, (nick_regex, nick_template, key, replacement), category=category, **kwargs) + nick_regex, nick_template = initialize_nick_templates( + pattern, replacement, pattern_is_regex=pattern_is_regex) + super().add(pattern, (nick_regex, nick_template, pattern, replacement), + category=category, **kwargs) def remove(self, key, category="inputline", **kwargs): """ diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index c04f5fa3fd..271e72211d 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -4,7 +4,9 @@ Unit tests for typeclass base system """ from django.test import override_settings from evennia.utils.test_resources import EvenniaTest +from evennia.typeclasses import attributes from mock import patch +from parameterized import parameterized # ------------------------------------------------------------ # Manager tests @@ -171,3 +173,37 @@ class TestTags(EvenniaTest): def test_does_not_have_tag_category_only(self): self.obj1.tags.add("tagC", "categoryC") self.assertFalse(self.obj1.tags.has(category="categoryD")) + + +class TestNickHandler(EvenniaTest): + """ + Test the nick handler mechanisms. + + """ + @parameterized.expand([ + # ("gr $1 $2 at $3", "emote with a $1 smile, $2 grins at $3.", False, + # "gr happy Foo at Bar", "emote with a happy smile, Foo grins at Bar."), + # ("gr (?P.+?) (?P.+?) at (?P.+?)", + # "emote with a $1 smile, $2 grins at $3.", True, + # "gr happy Foo at Bar", "emote with a happy smile, Foo grins at Bar."), + ("groo $1", "channel groo = $1", True, + "groo Hello world", "channel groo = Hello world"), + (r"groo\s*?(?P.*?)", "channel groo = $1", False, + "groo Hello world", "channel groo = Hello world"), + (r"groo\s*?(?P.*?)", "channel groo = $1", False, + "groo ", "channel groo = "), + (r"groo\s*?(?P.*?)", "channel groo = $1", False, + "groo", "channel groo = "), + ]) + def test_nick_parsing(self, pattern, replacement, pattern_is_regex, + inp_string, expected_replaced): + """ + Setting up nick patterns and make sure they replace as expected. + + """ + self.char1.nicks.add(pattern, replacement, + category="inputline", pattern_is_regex=pattern_is_regex) + actual_replaced = self.char1.nicks.nickreplace(inp_string) + + self.assertEqual(expected_replaced, actual_replaced) + self.char1.nicks.clear()