Browse Source

[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.
pull/95/head
Rapptz 9 years ago
parent
commit
ec6b1997ad
  1. 110
      discord/ext/commands/bot.py
  2. 24
      discord/ext/commands/core.py

110
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

24
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))

Loading…
Cancel
Save