From ec6b1997adfb6b190ff651c78dfe92dce36ffb61 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 9 Jan 2016 02:18:03 -0500 Subject: [PATCH] [commands] Add support for cogs. Cogs are basically class instances that have commands and event listeners. They allow for better organisation and grouping of commands and state. Similar to subclassing discord.Client. --- discord/ext/commands/bot.py | 110 +++++++++++++++++++++++++++++++++-- discord/ext/commands/core.py | 24 +++++++- 2 files changed, 128 insertions(+), 6 deletions(-) diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 5387df1a4..fbdaa75ab 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -28,7 +28,7 @@ import asyncio import discord import inspect -from .core import GroupMixin +from .core import GroupMixin, Command from .view import StringView from .context import Context from .errors import CommandNotFound @@ -67,6 +67,7 @@ class Bot(GroupMixin, discord.Client): super().__init__(**options) self.command_prefix = command_prefix self.extra_events = {} + self.cogs = {} # internal helpers @@ -231,20 +232,40 @@ class Bot(GroupMixin, discord.Client): name = func.__name__ if name is None else name if not asyncio.iscoroutinefunction(func): - func = asyncio.coroutine(func) + raise discord.ClientException('Listeners must be coroutines') if name in self.extra_events: self.extra_events[name].append(func) else: self.extra_events[name] = [func] + def remove_listener(self, func, name=None): + """Removes a listener from the pool of listeners. + + Parameters + ----------- + func + The function that was used as a listener to remove. + name + The name of the event we want to remove. Defaults to + ``func.__name__``. + """ + + name = func.__name__ if name is None else name + + if name in self.extra_events: + try: + self.extra_events[name].remove(func) + except ValueError: + pass + + def listen(self, name=None): """A decorator that registers another function as an external event listener. Basically this allows you to listen to multiple events from different places e.g. such as :func:`discord.on_ready` - If the function being listened to is not a coroutine, it makes it into - a coroutine a la :meth:`Client.async_event`. + The functions being listened to must be a coroutine. Examples --------- @@ -262,6 +283,11 @@ class Bot(GroupMixin, discord.Client): print('two') Would print one and two in an unspecified order. + + Raises + ------- + discord.ClientException + The function being listened to is not a coroutine. """ def decorator(func): @@ -270,6 +296,82 @@ class Bot(GroupMixin, discord.Client): return decorator + # cogs + + def add_cog(self, cog): + """Adds a "cog" to the bot. + + A cog is a class that has its own event listeners and commands. + + They are meant as a way to organize multiple relevant commands + into a singular class that shares some state or no state at all. + + More information will be documented soon. + + Parameters + ----------- + cog + The cog to register to the bot. + """ + + self.cogs[type(cog).__name__] = cog + members = inspect.getmembers(cog) + for name, member in members: + # register commands the cog has + if isinstance(member, Command): + member.instance = cog + if member.parent is None: + self.add_command(member) + continue + + # register event listeners the cog has + if name.startswith('on_'): + self.add_listener(member) + + def get_cog(self, name): + """Gets the cog instance requested. + + If the cog is not found, ``None`` is returned instead. + + Parameters + ----------- + name : str + The name of the cog you are requesting. + """ + return self.cogs.get(name) + + def remove_cog(self, name): + """Removes a cog the bot. + + All registered commands and event listeners that the + cog has registered will be removed as well. + + If no cog is found then ``None`` is returned, otherwise + the cog instance that is being removed is returned. + + Parameters + ----------- + name : str + The name of the cog to remove. + """ + + cog = self.cogs.pop(name, None) + if cog is None: + return cog + + members = inspect.getmembers(cog) + for name, member in members: + # remove commands the cog has + if isinstance(member, Command): + member.instance = None + if member.parent is None: + self.remove_command(member.name) + continue + + # remove event listeners the cog has + if name.startswith('on_'): + self.remove_listener(member) + # command processing @asyncio.coroutine diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index b86211dee..b73f2ac84 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -71,6 +71,9 @@ class Command: If the command is invoked while it is disabled, then :exc:`DisabledCommand` is raised to the :func:`on_command_error` event. Defaults to ``True``. + parent : Optional[command] + The parent command that this command belongs to. ``None`` is there + isn't one. checks A list of predicates that verifies if the command could be executed with the given :class:`Context` as the sole parameter. If an exception @@ -90,6 +93,9 @@ class Command: signature = inspect.signature(callback) self.params = signature.parameters.copy() self.checks = kwargs.get('checks', []) + self.module = inspect.getmodule(callback) + self.instance = None + self.parent = None def _receive_item(self, message, argument, regex, receiver, generator): match = re.match(regex, argument) @@ -181,14 +187,25 @@ class Command: def _parse_arguments(self, ctx): try: - ctx.args = [] + ctx.args = [] if self.instance is None else [self.instance] ctx.kwargs = {} args = ctx.args kwargs = ctx.kwargs first = True view = ctx.view - for name, param in self.params.items(): + iterator = iter(self.params.items()) + + if self.instance is not None: + # we have 'self' as the first parameter so just advance + # the iterator and resume parsing + try: + next(iterator) + except StopIteration: + fmt = 'Callback for {0.name} command is missing "self" parameter.' + raise discord.ClientException(fmt.format(self)) + + for name, param in iterator: if first and self.pass_context: args.append(ctx) first = False @@ -272,6 +289,9 @@ class GroupMixin: if not isinstance(command, Command): raise TypeError('The command passed must be a subclass of Command') + if isinstance(self, Command): + command.parent = self + if command.name in self.commands: raise discord.ClientException('Command {0.name} is already registered.'.format(command))