From 631f0e386430d9e864e367a45a74ad3f950a2fa4 Mon Sep 17 00:00:00 2001
From: Andrei <b1naryth1ef@gmail.com>
Date: Fri, 7 Oct 2016 13:23:19 -0500
Subject: [PATCH] Add plugin reloading, improve CLI interface some more

---
 README.md                |  7 +------
 disco/bot/bot.py         | 30 +++++++++++++++++++++++++++++-
 disco/bot/plugin.py      | 26 ++++++++++----------------
 disco/cli.py             | 20 +++++++++++++++++---
 disco/gateway/events.py  | 14 +++++++++++++-
 examples/__init__.py     |  0
 examples/basic_plugin.py | 17 ++++++++---------
 requirements.txt         |  2 +-
 8 files changed, 79 insertions(+), 37 deletions(-)
 create mode 100644 examples/__init__.py

diff --git a/README.md b/README.md
index f11a3fa..027229d 100644
--- a/README.md
+++ b/README.md
@@ -44,16 +44,11 @@ class SimplePlugin(Plugin):
     @Plugin.command('echo', '<content:str...>')
     def on_echo_command(self, event, content):
         event.msg.reply(content)
-
-if __name__ == '__main__':
-    Bot.from_cli(
-        SimplePlugin
-    ).run_forever()
 ```
 
 Using the default bot configuration, we can now run this script like so:
 
-`./simple.py --token="MY_DISCORD_TOKEN"`
+`python -m disco.cli --token="MY_DISCORD_TOKEN" --bot --plugin simpleplugin`
 
 And commands can be triggered by mentioning the bot (configued by the BotConfig.command\_require\_mention flag):
 
diff --git a/disco/bot/bot.py b/disco/bot/bot.py
index f8633b7..a28263e 100644
--- a/disco/bot/bot.py
+++ b/disco/bot/bot.py
@@ -1,5 +1,10 @@
 import re
+import importlib
+import inspect
 
+from six.moves import reload_module
+
+from disco.bot.plugin import Plugin
 from disco.bot.command import CommandEvent
 
 
@@ -261,12 +266,35 @@ class Bot(object):
             raise Exception('Cannot remove non-existant plugin: {}'.format(cls.__name__))
 
         self.plugins[cls.__name__].unload()
-        self.plugins[cls.__name__].destroy()
         del self.plugins[cls.__name__]
         self.compute_command_matches_re()
 
+    def reload_plugin(self, cls):
+        """
+        Reloads a plugin.
+        """
+        config = self.plugins[cls.__name__].config
+
+        self.rmv_plugin(cls)
+        module = reload_module(inspect.getmodule(cls))
+        self.add_plugin(getattr(module, cls.__name__), config)
+
     def run_forever(self):
         """
         Runs this bots core loop forever
         """
         self.client.run_forever()
+
+    def add_plugin_module(self, path, config=None):
+        """
+        Adds and loads a plugin, based on its module path.
+        """
+
+        mod = importlib.import_module(path)
+
+        for entry in map(lambda i: getattr(mod, i), dir(mod)):
+            if inspect.isclass(entry) and issubclass(entry, Plugin):
+                self.add_plugin(entry, config)
+                break
+        else:
+            raise Exception('Could not find any plugins to load within module {}'.format(path))
diff --git a/disco/bot/plugin.py b/disco/bot/plugin.py
index 4554835..a9714f7 100644
--- a/disco/bot/plugin.py
+++ b/disco/bot/plugin.py
@@ -124,6 +124,7 @@ class Plugin(LoggingClass, PluginDeco):
         self.state = bot.client.state
         self.config = config
 
+    def bind_all(self):
         self.listeners = []
         self.commands = {}
         self.schedules = {}
@@ -226,28 +227,21 @@ class Plugin(LoggingClass, PluginDeco):
 
         self.schedules[func.__name__] = gevent.spawn(repeat)
 
-    def destroy(self):
-        """
-        Destroys the plugin, removing all listeners and schedules. Called after
-        unload.
-        """
-        for listener in self.listeners:
-            listener.remove()
-
-        for schedule in self.schedules.values():
-            schedule.kill()
-
-        self.listeners = []
-        self.schedules = {}
-
     def load(self):
         """
         Called when the plugin is loaded
         """
-        pass
+        self.bind_all()
 
     def unload(self):
         """
         Called when the plugin is unloaded
         """
-        pass
+        for listener in self.listeners:
+            listener.remove()
+
+        for schedule in self.schedules.values():
+            schedule.kill()
+
+    def reload(self):
+        self.bot.reload_plugin(self.__class__)
diff --git a/disco/cli.py b/disco/cli.py
index db13c61..0c11d52 100644
--- a/disco/cli.py
+++ b/disco/cli.py
@@ -18,11 +18,13 @@ parser.add_argument('--shard-id', help='Current shard number/id', default=0)
 parser.add_argument('--manhole', action='store_true', help='Enable the manhole', default=False)
 parser.add_argument('--manhole-bind', help='host:port for the manhole to bind too', default='localhost:8484')
 parser.add_argument('--encoder', help='encoder for gateway data', default='json')
+parser.add_argument('--bot', help='run a disco bot on this client', action='store_true', default=False)
+parser.add_argument('--plugin', help='load plugins into the bot', nargs='*', default=[])
 
 logging.basicConfig(level=logging.INFO)
 
 
-def disco_main():
+def disco_main(run=False):
     """
     Creates an argument parser and parses a standard set of command line arguments,
     creating a new :class:`Client`.
@@ -35,6 +37,7 @@ def disco_main():
     args = parser.parse_args()
 
     from disco.client import Client, ClientConfig
+    from disco.bot import Bot
     from disco.gateway.encoding import ENCODERS
     from disco.util.token import is_valid_token
 
@@ -50,7 +53,18 @@ def disco_main():
     cfg.manhole_bind = args.manhole_bind
     cfg.encoding_cls = ENCODERS[args.encoder]
 
-    return Client(cfg)
+    client = Client(cfg)
+
+    if args.bot:
+        bot = Bot(client)
+
+        for plugin in args.plugin:
+            bot.add_plugin_module(plugin)
+
+    if run:
+        client.run_forever()
+
+    return client
 
 if __name__ == '__main__':
-    disco_main().run_forever()
+    disco_main(True)
diff --git a/disco/gateway/events.py b/disco/gateway/events.py
index fd250a1..a58f62d 100644
--- a/disco/gateway/events.py
+++ b/disco/gateway/events.py
@@ -5,10 +5,19 @@ from disco.types import Guild, Channel, User, GuildMember, Role, Message, VoiceS
 from disco.types.base import Model, Field, snowflake, listof, text
 
 
-# TODO: clean this... use BaseType, etc
 class GatewayEvent(Model):
+    """
+    The GatewayEvent class wraps various functionality for events passed to us
+    over the gateway websocket, and serves as a simple proxy to inner values for
+    some wrapped event-types (e.g. MessageCreate only contains a message, so we
+    proxy all attributes to the inner message object).
+    """
+
     @staticmethod
     def from_dispatch(client, data):
+        """
+        Create a new GatewayEvent instance based on event data.
+        """
         cls = globals().get(inflection.camelize(data['t'].lower()))
         if not cls:
             raise Exception('Could not find cls for {}'.format(data['t']))
@@ -17,6 +26,9 @@ class GatewayEvent(Model):
 
     @classmethod
     def create(cls, obj, client):
+        """
+        Create this GatewayEvent class from data and the client.
+        """
         # If this event is wrapping a model, pull its fields
         if hasattr(cls, '_wraps_model'):
             alias, model = cls._wraps_model
diff --git a/examples/__init__.py b/examples/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/examples/basic_plugin.py b/examples/basic_plugin.py
index 8796745..5d8441f 100644
--- a/examples/basic_plugin.py
+++ b/examples/basic_plugin.py
@@ -3,12 +3,15 @@ import sys
 import json
 
 from disco import VERSION
-from disco.cli import disco_main
-from disco.bot import Bot, Plugin
-from disco.types.permissions import Permissions
+from disco.bot import Plugin
 
 
 class BasicPlugin(Plugin):
+    @Plugin.command('reload')
+    def on_reload(self, event):
+        self.reload()
+        event.msg.reply('Reloaded!')
+
     @Plugin.listen('MessageCreate')
     def on_message_create(self, msg):
         self.log.info('Message created: {}: {}'.format(msg.author, msg.content))
@@ -82,7 +85,8 @@ class BasicPlugin(Plugin):
 
     @Plugin.command('lol')
     def on_lol(self, event):
-        event.msg.reply("{}".format(event.channel.can(event.msg.author, Permissions.MANAGE_EMOJIS)))
+        event.msg.reply(':^)')
+        # event.msg.reply("{}".format(event.channel.can(event.msg.author, Permissions.MANAGE_EMOJIS)))
 
     @Plugin.command('perms')
     def on_perms(self, event):
@@ -90,8 +94,3 @@ class BasicPlugin(Plugin):
         event.msg.reply('```json\n{}\n```'.format(
             json.dumps(perms.to_dict(), sort_keys=True, indent=2, separators=(',', ': '))
         ))
-
-if __name__ == '__main__':
-    bot = Bot(disco_main())
-    bot.add_plugin(BasicPlugin)
-    bot.run_forever()
diff --git a/requirements.txt b/requirements.txt
index b67520a..e4d2570 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 gevent==1.1.2
-holster==1.0.3
+holster==1.0.4
 inflection==0.3.1
 requests==2.11.1
 six==1.10.0