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