From 65459051dad8fe182099951874ddfbd986303d6b Mon Sep 17 00:00:00 2001 From: dave Date: Sun, 14 May 2017 12:44:14 -0700 Subject: [PATCH] Support hook decorators --- docs/module_guide/_module_guide.rst | 39 +++++++++++++++++---------- pyircbot/modulebase.py | 41 +++++++++++++++++++++++++++++ pyircbot/modules/ASCII.py | 4 +-- 3 files changed, 68 insertions(+), 16 deletions(-) diff --git a/docs/module_guide/_module_guide.rst b/docs/module_guide/_module_guide.rst index 1331801..e1241c3 100644 --- a/docs/module_guide/_module_guide.rst +++ b/docs/module_guide/_module_guide.rst @@ -13,13 +13,14 @@ file's name. .. code-block:: python - from pyircbot.modulebase import ModuleBase,ModuleHook + from pyircbot.modulebase import ModuleBase, hook class EchoExample(ModuleBase): The class's ``__init__`` method accepts 2 args - a reference to the bot's API and what the bot has decided to name this module. These are passed to ModuleBase. Module's init method should be as quick as possible. The bot loads -modules one after the other so a long delay slows bot startup. +modules sequentially on startup and you should avoid long operations here as it +will undesirably slow bot startup time. .. code-block:: python @@ -32,20 +33,29 @@ calling :py:meth:`pyircbot.modulebase.ModuleBase.loadConfig`: .. code-block:: python - self.loadConfig() print(self.config) + self.loadConfig() # Manually reload config -In ``__init__``, the module lists what hooks it wants to listen for. Hooks are -executed when the corresponding IRC protocol command is received. - -.. code-block:: python - - self.hooks=[ModuleHook("PRIVMSG", self.echo)] - -Then, a handler for this hook: +Any other setup code should be placed in ``__init__``. Note that ``__init__`` +will be executed when the bot is starting up and before the irc connection is +opened. + +Adding interaction +------------------ + +In order to make your module respond to various IRC commands, pyircbot uses a +system of "hooks", which can be defined using decorators or programmatically. +Note: in this case, "commands" refers to IRC protocol commands such as PRIVMSG, +KICK, JOIN, etc. Pyircbot also provides some meta-events that are accessed in +the same way. The complete list of supported commands can be seen in the source +of :py:meth:`pyircbot.irccore.IRCCore.initHooks`. + +The easiest method is to use the ``hook`` decorator on any function your want +called when an IRC command is received. .. code-block:: python + @hook("PRIVMSG") def echo(self, event): The handler is passed and IRCEvent object containing the data sent by the irc @@ -88,17 +98,18 @@ shutdown handler is needed to ensure a clean shutdown. EchoExample module ------------------ +This is the snippets above combined into a usable module. + .. code-block:: python - from pyircbot.modulebase import ModuleBase,ModuleHook + from pyircbot.modulebase import ModuleBase, hook class EchoExample(ModuleBase): def __init__(self, bot, moduleName): ModuleBase.__init__(self, bot, moduleName) - self.loadConfig() print(self.config) - self.hooks=[ModuleHook("PRIVMSG", self.echo)] + @hook("PRIVMSG") def echo(self, event): self.bot.act_PRIVMSG(event.args[0], event.trailing) diff --git a/pyircbot/modulebase.py b/pyircbot/modulebase.py index b18e023..dd8a424 100644 --- a/pyircbot/modulebase.py +++ b/pyircbot/modulebase.py @@ -42,8 +42,21 @@ class ModuleBase: # Autoload config if available self.loadConfig() + # Prepare any function hooking + self.init_hooks() + self.log.info("Loaded module %s" % self.moduleName) + def init_hooks(self): + """ + Scan the module for tagged methods and set up appropriate protocol hooks. + """ + for attr_name in dir(self): + attr = getattr(self, attr_name) + if callable(attr) and hasattr(attr, ATTR_ACTION_HOOK): + for action in getattr(attr, ATTR_ACTION_HOOK): + self.hooks.append(ModuleHook(action, attr)) + def loadConfig(self): """ Loads this module's config into self.config. The bot's main config is checked for a section matching the module @@ -79,3 +92,31 @@ class ModuleHook: def __init__(self, hook, method): self.hook = hook self.method = method + + +ATTR_ACTION_HOOK = "__tag_hooks" + + +class hook(object): + """ + Decorating for calling module methods in response to IRC actions. Example: + ``` + @hook("PRIVMSG") + def mymyethod(self, message): + print("IRC server sent PRIVMSG") + ``` + This stores a list of IRC actions each function is tagged for in method.__tag_hooks. This attribute is scanned + during module init and appropriate hooks are set up. + """ + 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 diff --git a/pyircbot/modules/ASCII.py b/pyircbot/modules/ASCII.py index 30b0f52..27e5016 100644 --- a/pyircbot/modules/ASCII.py +++ b/pyircbot/modules/ASCII.py @@ -5,7 +5,7 @@ :synopsis: Spam chat with awesome ascii texts """ -from pyircbot.modulebase import ModuleBase, ModuleHook +from pyircbot.modulebase import ModuleBase, hook from threading import Thread from glob import iglob from collections import defaultdict @@ -20,10 +20,10 @@ RE_ASCII_FNAME = re.compile(r'^[a-zA-Z0-9\-_]+$') class ASCII(ModuleBase): def __init__(self, bot, moduleName): ModuleBase.__init__(self, bot, moduleName) - self.hooks.append(ModuleHook("PRIVMSG", self.listen_msg)) self.running_asciis = defaultdict(lambda: None) self.killed_channels = defaultdict(lambda: False) + @hook("PRIVMSG") def listen_msg(self, msg): """ Handle commands