From 5c8f6b02fdab265be1c85140bd649fbbafcee8b8 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 22 Nov 2017 20:20:52 -0800 Subject: [PATCH] Refactor modulebase hooks to reduce internal complexity --- pyircbot/modulebase.py | 139 +++++++++++++++++++------------------- pyircbot/modules/ASCII.py | 8 +-- pyircbot/modules/Calc.py | 4 +- pyircbot/modules/SMS.py | 2 +- pyircbot/pyircbot.py | 11 +-- 5 files changed, 81 insertions(+), 83 deletions(-) diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index 53517ab..46a3529 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -10,7 +10,6 @@ import re import os import logging from .pyircbot import PyIRCBot -from inspect import getargspec class ModuleBase: @@ -30,7 +29,7 @@ class ModuleBase: """Reference to the master PyIRCBot object""" self.hooks = [] - """Hooks (aka listeners) this module has""" + """Low-level protocol hooks this module has""" self.irchooks = [] """IRC Hooks this module has""" @@ -61,12 +60,9 @@ class ModuleBase: attr = getattr(self, attr_name) if not callable(attr): continue - if hasattr(attr, ATTR_ACTION_HOOK): - for action in getattr(attr, ATTR_ACTION_HOOK): - self.hooks.append(ModuleHook(action, attr)) - if hasattr(attr, ATTR_COMMAND_HOOK): - for action in getattr(attr, ATTR_COMMAND_HOOK): - self.irchooks.append(IRCHook(action, attr)) + if hasattr(attr, ATTR_ALL_HOOKS): + for hook in getattr(attr, ATTR_ALL_HOOKS): + self.irchooks.append(IRCHook(hook.validate, attr)) def loadConfig(self): """ @@ -110,19 +106,61 @@ class ModuleHook: class IRCHook: - def __init__(self, hook, method): - self.hook = hook + def __init__(self, validator, method): + """ + :param validator: method accpeting an IRCEvent and returning false-like or true-like depending on match + :param method: module method + """ + self.validator = validator self.method = method - def call(self, msg): - self.hook.call(self.method, msg) + +ATTR_ALL_HOOKS = "__hooks" -ATTR_ACTION_HOOK = "__tag_hooks" -ATTR_COMMAND_HOOK = "__tag_commands" +class AbstractHook(object): + """ + Decorator for calling module methods in response to arbitrary IRC actions. Example: + + .. code-block:: python + + @myhooksubclass() + def mymyethod(self, message, extra): + print("IRC server sent something that matched ") + + This stores some record of the above filtering in an attribute of the decorated method, such as method.__tag_hooks. + This attribute is scanned during module init and appropriate hooks are set up. + + Hooks implement a validate() method that return a true-like or false-like item, dictating whether the hooked method + will be called (with the message and true-like object as parameters) + + :param args: irc protocol event to listen for. See :py:meth:`pyircbot.irccore.IRCCore.initHooks` for a complete list + :type args: str + """ + def __init__(self): + # todo do i need this here for the docstring? + pass + + def __call__(self, func): + """ + Store a list of such hooks in an attribute of the decorated method + """ + if not hasattr(func, ATTR_ALL_HOOKS): + setattr(func, ATTR_ALL_HOOKS, [self]) + else: + getattr(func, ATTR_ALL_HOOKS).extend(self) + return func + + def validate(self, msg, bot): + """ + Return a true-like item if the hook matched. Otherwise, false. + :param msg: IRCEvent instance of the message + :param bot: reference to the bot TODO remove this + """ + return True -class hook(object): +class hook(AbstractHook): """ Decorator for calling module methods in response to IRC actions. Example: @@ -141,36 +179,12 @@ class hook(object): def __init__(self, *args): self.commands = args - def __call__(self, func): - if not hasattr(func, ATTR_ACTION_HOOK): - setattr(func, ATTR_ACTION_HOOK, list(self.commands)) - else: - getattr(func, ATTR_ACTION_HOOK).extend(self.commands) - return func - - -class irchook(object): - def __call__(self, func): - if not hasattr(func, ATTR_COMMAND_HOOK): - setattr(func, ATTR_COMMAND_HOOK, [self]) - else: - getattr(func, ATTR_COMMAND_HOOK).extend(self) - return func - - def call(self, method, msg): - """ - Call the hooked function - """ - method(msg) # TODO is this actually a sane base? - def validate(self, msg, bot): - """ - Return True if the message should be passed on. False otherwise. - """ - return True + if msg.command in self.commands: + return True -class command(irchook): +class command(hook): """ Decorator for calling module methods when a command is parsed from chat @@ -197,20 +211,12 @@ class command(irchook): """ def __init__(self, *keywords, require_args=False, allow_private=False): + super().__init__("PRIVMSG") self.keywords = keywords self.require_args = require_args self.allow_private = allow_private self.parsed_cmd = None - def call(self, method, msg): - """ - Overridden to make the msg param optional - """ - if len(getargspec(method).args) == 3: - return method(self.parsed_cmd, msg) - else: - return method(self.parsed_cmd) - def validate(self, msg, bot): """ Test a message and return true if matched. @@ -220,23 +226,22 @@ class command(irchook): :param bot: reference to main pyircbot :type bot: pyircbot.pyircbot.PyIRCBot """ + if not super().validate(msg, bot): + return False if not self.allow_private and msg.args[0] == "#": return False for keyword in self.keywords: - if self._validate_one(msg, keyword): - return True + single = self._validate_one(msg, keyword) + if single: + return single return False def _validate_one(self, msg, keyword): with_prefix = "{}{}".format(self.prefix, keyword) - cmd = PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) - if cmd: - self.parsed_cmd = cmd - return True - return False + return PyIRCBot.messageHasCommand(with_prefix, msg.trailing, requireArgs=self.require_args) -class regex(irchook): +class regex(hook): """ Decorator for calling module methods when a message matches a regex. @@ -258,20 +263,11 @@ class regex(irchook): """ def __init__(self, *regexps, allow_private=False, types=None): + super().__init__("PRIVMSG") self.regexps = [re.compile(r) for r in regexps] self.allow_private = allow_private - self.matches = None self.types = types - def call(self, method, msg): - """ - Overridden to pass matches and make an arg optional - """ - if len(getargspec(method).args) == 3: - return method(self.matches, msg) - else: - return method(self.matches) - def validate(self, msg, bot): """ Test a message and return true if matched. @@ -281,6 +277,8 @@ class regex(irchook): :param bot: reference to main pyircbot :type bot: pyircbot.pyircbot.PyIRCBot """ + if not super().validate(msg, bot): + return False if self.types and msg.command not in self.types: return False if not self.allow_private and msg.args[0] == "#": @@ -288,8 +286,7 @@ class regex(irchook): for exp in self.regexps: matches = exp.search(msg.trailing) if matches: - self.matches = matches - return True + return matches return False diff --git a/pyircbot/modules/ASCII.py b/pyircbot/modules/ASCII.py index 94e61a4..124a663 100644 --- a/pyircbot/modules/ASCII.py +++ b/pyircbot/modules/ASCII.py @@ -33,7 +33,7 @@ class ASCII(ModuleBase): # @hook("PRIVMSG") @command("listascii") - def cmd_listascii(self, cmd, msg): + def cmd_listascii(self, msg, cmd): """ List available asciis """ @@ -48,7 +48,7 @@ class ASCII(ModuleBase): return @command("ascii", require_args=True) - def cmd_ascii(self, cmd, msg): + def cmd_ascii(self, msg, cmd): # import ipdb # ipdb.set_trace() # Send out an ascii @@ -70,7 +70,7 @@ class ASCII(ModuleBase): return @command("stopascii") - def cmd_stopascii(self, cmd, msg): + def cmd_stopascii(self, msg, cmd): """ Command to stop the running ascii in a given channel """ @@ -120,7 +120,7 @@ class ASCII(ModuleBase): del self.killed_channels[channel] @command("asciiedit", require_args=True) - def cmd_asciiedit(self, cmd, msg): + def cmd_asciiedit(self, msg, cmd): ascii_name = cmd.args.pop(0) try: with open(self.getFilePath(ascii_name + ".json")) as f: diff --git a/pyircbot/modules/Calc.py b/pyircbot/modules/Calc.py index afca135..4cf0fc7 100644 --- a/pyircbot/modules/Calc.py +++ b/pyircbot/modules/Calc.py @@ -80,7 +80,7 @@ class Calc(ModuleBase): return "Please wait %s minute(s) and %s second(s)." % (minutes, seconds) @regex(r'(?:^\.?(?:calc|quote)(?:\s+?(?:([^=]+)(?:\s?(=)\s?(.+)?)?)?)?)', types=['PRIVMSG']) - def cmd_calc(self, match, message): + def cmd_calc(self, message, match): word, changeit, value = match.groups() if word: word = word.strip() @@ -134,7 +134,7 @@ class Calc(ModuleBase): self.updateTimeSince(channel, "calc") @command("match", require_args=True) - def cmd_match(self, cmd, msg): + def cmd_match(self, msg, cmd): if self.config["delayMatch"] > 0 and self.timeSince(msg.args[0], "match") < self.config["delayMatch"]: self.bot.act_PRIVMSG(msg.prefix.nick, self.remainingToStr(self.config["delayMatch"], self.timeSince(msg.args[0], "match"))) diff --git a/pyircbot/modules/SMS.py b/pyircbot/modules/SMS.py index 4a83ce9..6237b13 100644 --- a/pyircbot/modules/SMS.py +++ b/pyircbot/modules/SMS.py @@ -134,7 +134,7 @@ class SMS(ModuleBase): cherrypy.engine.exit() @regex(r'(?:^\.text\-([a-zA-Z0-9]+)(?:\s+(.+))?)', types=['PRIVMSG']) - def cmd_text(self, match, msg): + def cmd_text(self, msg, match): """ Text somebody """ diff --git a/pyircbot/pyircbot.py b/pyircbot/pyircbot.py index 587662a..e352dee 100644 --- a/pyircbot/pyircbot.py +++ b/pyircbot/pyircbot.py @@ -69,7 +69,7 @@ class PyIRCBot(object): self.initModules() # Internal usage hook - self.irc.addHook("PRIVMSG", self._hook_privmsg_internal) + self.irc.addHook("PRIVMSG", self._irchook_internal) def run(self): self.loop = asyncio.get_event_loop() @@ -116,15 +116,16 @@ class PyIRCBot(object): for modulename in self.botconfig["modules"]: self.loadmodule(modulename) - def _hook_privmsg_internal(self, msg): + def _irchook_internal(self, msg): """ - IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a PRIVMSG is + IRC hook handler. Calling point for IRCHook based module hooks. This method is called when a message is received. It tests all hooks in all modules against the message can calls the hooked function on hits. """ for module_name, module in self.moduleInstances.items(): for hook in module.irchooks: - if hook.hook.validate(msg, self): - hook.call(msg) + validation = hook.validator(msg, self) + if validation: + hook.method(msg, validation) def importmodule(self, name): """Import a module