From a10a297c5522cb863daeea6a11f8dd7a56a18b92 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 25 Apr 2021 16:14:43 +0200 Subject: [PATCH] Fix alias regexes --- evennia/commands/default/comms.py | 42 ++++++++++++-------- evennia/typeclasses/attributes.py | 64 ++++++++++++++++++++----------- evennia/typeclasses/tests.py | 36 ++++++++++++----- 3 files changed, 93 insertions(+), 49 deletions(-) diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index 49b397e452..adb4c7d5a9 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -16,7 +16,6 @@ from evennia.locks.lockhandler import LockException from evennia.utils import create, logger, utils from evennia.utils.logger import tail_log_file from evennia.utils.utils import make_iter, class_from_module -from evennia.utils import evmore from evennia.utils.evmenu import ask_yes_no COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -86,10 +85,12 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): switch_options = ( "list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias", "create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",) + # disable this in child command classes if wanting on-character channels + account_caller = True # note - changing this will invalidate existing aliases in db # channel_msg_nick_alias = r"{alias}\s*?(?P.+?){{0,1}}" - channel_msg_nick_alias = r"{alias} (?P.+?)" + channel_msg_nick_alias = r"{alias}\s*?|{alias}\s+?(?P.+?)" channel_msg_nick_replacement = "channel {channelname} = $1" def search_channel(self, channelname, exact=False): @@ -193,13 +194,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): return result, "" if result else f"Were not allowed to subscribe to channel {channel.key}" - def unsub_from_channel(self, channel): + def unsub_from_channel(self, channel, **kwargs): """ Un-Subscribe to a channel. Note that all permissions should be checked before this step. Args: channel (Channel): The channel to unsub from. + **kwargs: Passed on to nick removal. Returns: bool, str: True, None if un-connection succeeded. If False, @@ -210,9 +212,13 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): if not channel.has_connection(caller): return False, f"Not listening to channel {channel.key}." - # clear nicks + # clear aliases for key_or_alias in self.get_channel_aliases(channel): - self.remove_alias(key_or_alias) + self.remove_alias(key_or_alias, **kwargs) + # remove the channel-name alias too + msg_alias = self.channel_msg_nick_alias.format(alias=channel.key.lower()) + caller.nicks.remove(msg_alias, category="inputline", **kwargs) + result = channel.disconnect(caller) return result, "" if result else f"Could not unsubscribe from channel {channel.key}" @@ -240,14 +246,15 @@ 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) + # the message-pattern allows us to type the channel on its own without + # needing to use the `channel` command explicitly. + msg_pattern = self.channel_msg_nick_alias.format(alias=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="inputline", - regex_pattern=True, **kwargs) + self.caller.nicks.add(msg_pattern, msg_replacement, category="inputline", + pattern_is_regex=True, **kwargs) def remove_alias(self, alias, **kwargs): """ @@ -270,8 +277,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): """ caller = self.caller 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) + msg_alias = self.channel_msg_nick_alias.format(alias=alias) caller.nicks.remove(msg_alias, category="inputline", **kwargs) return True, "" @@ -357,8 +364,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): lockstring = "send:all();listen:all();control:id(%s)" % caller.id new_chan = create.create_channel( - name, aliases=aliases,desc=description, locks=lockstring, - typeclass=typeclass) + name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass) new_chan.connect(caller) return new_chan, "" @@ -754,13 +760,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): subscribed, available = self.list_channels() if channel in subscribed: table = self.display_subbed_channels([channel]) - self.msg( - "\n|wSubscribed to Channel|n " - f"(use |w/all|n to see all available)\n{table}") + inputname = self.raw_cmdname + if inputname.lower() != channel.key.lower(): + header = f"Channel |w{inputname}|n (alias for {channel.key} channel)" + else: + header = f"Channel |w{channel.key}|n" + self.msg(f"{header}\n(use |w{inputname} |n to chat and " + f"the 'channel' command to customize)\n{table}") elif channel in available: table = self.display_all_channels([], [channel]) self.msg( - "\n|wNot subscribed Channel|n (use /list to " + "\n|wNot subscribed to this channel|n (use /list to " f"show all subscriptions)\n{table}") return diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index e68586e2e5..e55a3f51ce 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -1291,9 +1291,10 @@ Custom arg markers $N argument position (1-99) """ +_RE_OR = re.compile(r"(?.+?)`. Such - groups can then also be made optional using e.g. `{?P.*?}`. - out_template (str): The template to be used to replace the string + as matching groups named as `argN`, such as `(?P.+?)`. + replacement (str): The template to be used to replace the string matched by the pattern. This can contain `$N` markers and is never parsed into a regex. + pattern_is_regex (bool): If set, `pattern` is a full regex string + instead of containing shell patterns. Returns: regex, template (str): Regex to match against strings and template @@ -1325,35 +1327,47 @@ def initialize_nick_templates(pattern, out_template, pattern_is_regex=False): 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.` + Examples: + - `pattern` (shell syntax): `"grin $1"` + - `pattern` (regex): `"grin (?P)"` + - `replacement`: `"emote gives a wicked grin to $1"` """ # 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" + # Note that for a regex we can't validate in the way we do for the shell + # pattern, since you may have complex OR statements or optional arguments. + + # Explicit regex given from the onset - this already contains argN + # groups. we need to split out any | - separated parts so we can + # attach the line-break/ending extras all regexes require. + pattern_regex_string = r"|".join( + or_part + r"(?:[\n\r]*?)\Z" + for or_part in _RE_OR.split(pattern)) + else: - # regex generated by parsing shell pattern syntax - convert $N to argN groups + # Shell pattern syntax - convert $N to argN groups + # for the shell pattern we make sure we have matching $N on both sides + pattern_args = [match.group(1) for match in _RE_NICK_RAW_ARG.finditer(pattern)] + replacement_args = [ + match.group(1) for match in _RE_NICK_RAW_ARG.finditer(replacement)] + 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.") + + # generate regex from shell pattern 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" - - # 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.") + # we must account for a possible line break coming over the wire + pattern_regex_string = pattern_regex_string[:-2] + r"(?:[\n\r]*?)\Z" # 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) + replacement_string = _RE_NICK_RAW_ARG.sub(lambda m: "{arg%s}" % m.group(2), replacement) - return pattern_regex_string, template_string + return pattern_regex_string, replacement_string def parse_nick_template(string, template_regex, outtemplate): @@ -1366,12 +1380,16 @@ def parse_nick_template(string, template_regex, outtemplate): initialize_nick_template. outtemplate (str): The template to which to map the matches produced by the template_regex. This should have $1, $2, - etc to match the regex. + etc to match the template-regex. Un-found $N-markers (possible if + the regex has optional matching groups) are replaced with empty + strings. """ match = template_regex.match(string) if match: - return True, outtemplate.format(**match.groupdict()) + matchdict = {key: value if value is not None else "" + for key, value in match.groupdict().items()} + return True, outtemplate.format(**matchdict) return False, string diff --git a/evennia/typeclasses/tests.py b/evennia/typeclasses/tests.py index 271e72211d..021a3d7173 100644 --- a/evennia/typeclasses/tests.py +++ b/evennia/typeclasses/tests.py @@ -177,23 +177,38 @@ class TestTags(EvenniaTest): class TestNickHandler(EvenniaTest): """ - Test the nick handler mechanisms. + Test the nick handler replacement. """ @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, + # shell syntax + ("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."), + # regex syntax + ("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."), + # channel-style syntax + ("groo $1", "channel groo = $1", False, "groo Hello world", "channel groo = Hello world"), - (r"groo\s*?(?P.*?)", "channel groo = $1", False, + (r"groo\s*?|groo\s+?(?P.+?)", "channel groo = $1", True, "groo Hello world", "channel groo = Hello world"), - (r"groo\s*?(?P.*?)", "channel groo = $1", False, + (r"groo\s*?|groo\s+?(?P.+?)", "channel groo = $1", True, "groo ", "channel groo = "), - (r"groo\s*?(?P.*?)", "channel groo = $1", False, + (r"groo\s*?|groo\s*?(?P.+?)", "channel groo = $1", True, "groo", "channel groo = "), + (r"groo\s*?|groo\s+?(?P.+?)", "channel groo = $1", True, + "grooHello world", "grooHello world"), # not matched - this is correct! + # optional, space-separated arguments + (r"groo\s*?|groo\s+?(?P.+?)(?:\s+?(?P.+?)){0,1}", + "channel groo = $1 and $2", True, + "groo Hello World", "channel groo = Hello and World"), + (r"groo\s*?|groo\s+?(?P.+?)(?:\s+?(?P.+?)){0,1}", + "channel groo = $1 and $2", True, + "groo Hello", "channel groo = Hello and "), + (r"groo\s*?|groo\s+?(?P.+?)(?:\s+?(?P.+?)){0,1}", + "channel groo = $1 and $2", True, + "groo", "channel groo = and "), # $1/$2 replaced by '' ]) def test_nick_parsing(self, pattern, replacement, pattern_is_regex, inp_string, expected_replaced): @@ -201,6 +216,7 @@ class TestNickHandler(EvenniaTest): Setting up nick patterns and make sure they replace as expected. """ + # from evennia import set_trace;set_trace() self.char1.nicks.add(pattern, replacement, category="inputline", pattern_is_regex=pattern_is_regex) actual_replaced = self.char1.nicks.nickreplace(inp_string)