aboutsummaryrefslogtreecommitdiff
path: root/discord/ext/commands/cog.py
diff options
context:
space:
mode:
authorRapptz <[email protected]>2019-02-23 04:10:10 -0500
committerRapptz <[email protected]>2019-02-23 04:10:10 -0500
commitcaf3d17d4aa3ce45435bb96ea6d99317bfef8618 (patch)
treed8bff0494efd7ba51d011ab4b7a9d30009eeae6d /discord/ext/commands/cog.py
parent[commands] Fix up wording on HelpFormatter.get_ending_note (diff)
downloaddiscord.py-caf3d17d4aa3ce45435bb96ea6d99317bfef8618.tar.xz
discord.py-caf3d17d4aa3ce45435bb96ea6d99317bfef8618.zip
Rework entire cog system and partially document it and extensions.
Diffstat (limited to 'discord/ext/commands/cog.py')
-rw-r--r--discord/ext/commands/cog.py325
1 files changed, 325 insertions, 0 deletions
diff --git a/discord/ext/commands/cog.py b/discord/ext/commands/cog.py
new file mode 100644
index 00000000..1da0c69d
--- /dev/null
+++ b/discord/ext/commands/cog.py
@@ -0,0 +1,325 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License (MIT)
+
+Copyright (c) 2015-2017 Rapptz
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+"""
+
+import inspect
+import copy
+from ._types import _BaseCommand
+
+__all__ = ('CogMeta', 'Cog')
+
+class CogMeta(type):
+ """A怀metaclass for defining a cog.
+
+ Note that you should probably not use this directly. It is exposed
+ purely for documentation purposes along with making custom metaclasses to intermix
+ with other metaclasses such as the :class:`abc.ABCMeta` metaclass.
+
+ For example, to create an abstract cog mixin class, the following would be done.
+
+ .. code-block:: python3
+
+ import abc
+
+ class CogABCMeta(commands.CogMeta, abc.ABCMeta):
+ pass
+
+ class SomeMixin(metaclass=abc.ABCMeta):
+ pass
+
+ class SomeCogMixin(SomeMixin, commands.Cog, metaclass=CogABCMeta):
+ pass
+
+ .. note::
+
+ When passing an attribute of a metaclass that is documented below, note
+ that you must pass it as a keyword-only argument to the class creation
+ like the following example:
+
+ .. code-block:: python3
+
+ class MyCog(commands.Cog, name='My Cog'):
+ pass
+
+ Attributes
+ -----------
+ name: :class:`str`
+ The cog name. By default, it is the name of the class with no modification.
+ command_attrs: :class:`dict`
+ A list of attributes to apply to every command inside this cog. The dictionary
+ is passed into the :class:`Command` (or its subclass) options at ``__init__``.
+ If you specify attributes inside the command attribute in the class, it will
+ override the one specified inside this attribute. For example:
+
+ .. code-block:: python3
+
+ class MyCog(commands.Cog, command_attrs=dict(hidden=True)):
+ @commands.command()
+ async def foo(self, ctx):
+ pass # hidden -> True
+
+ @commands.command(hidden=False)
+ async def bar(self, ctx):
+ pass # hidden -> False
+ """
+
+ def __new__(cls, *args, **kwargs):
+ name, bases, attrs = args
+ attrs['__cog_name__'] = kwargs.pop('name', name)
+ attrs['__cog_settings__'] = command_attrs = kwargs.pop('command_attrs', {})
+
+ commands = []
+ listeners = []
+
+ for elem, value in attrs.items():
+ if isinstance(value, _BaseCommand):
+ commands.append(value)
+ elif inspect.iscoroutinefunction(value):
+ try:
+ is_listener = getattr(value, '__cog_listener__')
+ except AttributeError:
+ continue
+ else:
+ listeners.append((value.__cog_listener_name__, value.__name__))
+
+ attrs['__cog_commands__'] = commands # this will be copied in Cog.__new__
+ attrs['__cog_listeners__'] = tuple(listeners)
+ return super().__new__(cls, name, bases, attrs)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args)
+
+ @classmethod
+ def qualified_name(cls):
+ return cls.__cog_name__
+
+class Cog(metaclass=CogMeta):
+ """The base class that all cogs must inherit from.
+
+ A cog is a collection of commands, listeners, and optional state to
+ help group commands together. More information on them can be found on
+ the :ref:`ext_commands_cogs` page.
+
+ When inheriting from this class, the options shown in :class:`CogMeta`
+ are equally valid here.
+ """
+
+ def __new__(cls, *args, **kwargs):
+ # For issue 426, we need to store a copy of the command objects
+ # since we modify them to inject `self` to them.
+ # To do this, we need to interfere with the Cog creation process.
+ self = super().__new__(cls)
+ cmd_attrs = cls.__cog_settings__
+
+ # Either update the command with the cog provided defaults or copy it.
+ self.__cog_commands__ = tuple(c._update_copy(cmd_attrs) for c in cls.__cog_commands__)
+ return self
+
+ def get_commands(self):
+ r"""Returns a :class:`tuple` of :class:`.Command`\s and its subclasses that are
+ defined inside this cog.
+ """
+ return self.__cog_commands__
+
+ def walk_commands(self):
+ """An iterator that recursively walks through this cog's commands and subcommands."""
+ from .core import GroupMixin
+ for command in self.__cog_commands__:
+ yield command
+ if isinstance(command, GroupMixin):
+ yield from command.walk_commands()
+
+ def get_listeners(self):
+ """Returns a :class:`list` of (name, function) listener pairs that are defined in this cog."""
+ return [(name, getattr(self, method_name)) for name, method_name in self.__cog_listeners__]
+
+ @classmethod
+ def _get_overridden_method(cls, method):
+ """Return None if the method is not overridden. Otherwise returns the overridden method."""
+ if method.__func__ is getattr(cls, method.__name__):
+ return None
+ return method
+
+ @classmethod
+ def listener(cls, name=None):
+ """A decorator that marks a function as a listener.
+
+ This is the cog equivalent of :meth:`.Bot.listen`.
+
+ Parameters
+ ------------
+ name: :class:`str`
+ The name of the event being listened to. If not provided, it
+ defaults to the function's name.
+
+ Raises
+ --------
+ TypeError
+ The function is not a coroutine function.
+ """
+
+ def decorator(func):
+ if not inspect.iscoroutinefunction(func):
+ raise TypeError('Listener function must be a coroutine function.')
+ func.__cog_listener__ = True
+ func.__cog_listener_name__ = name or func.__name__
+ return func
+ return decorator
+
+ def cog_unload(self):
+ """A special method that is called when the cog gets removed.
+
+ This function **cannot** be a coroutine. It must be a regular
+ function.
+
+ Subclasses must replace this if they want special unloading behaviour.
+ """
+ pass
+
+ def bot_check_once(self, ctx):
+ """A special method that registers as a :meth:`.Bot.check_once`
+ check.
+
+ This function **can** be a coroutine and must take a sole parameter,
+ ``ctx``, to represent the :class:`.Context`.
+ """
+ return True
+
+ def bot_check(self, ctx):
+ """A special method that registers as a :meth:`.Bot.check`
+ check.
+
+ This function **can** be a coroutine and must take a sole parameter,
+ ``ctx``, to represent the :class:`.Context`.
+ """
+ return True
+
+ def cog_check(self, ctx):
+ """A special method that registers as a :func:`commands.check`
+ for every command and subcommand in this cog.
+
+ This function **can** be a coroutine and must take a sole parameter,
+ ``ctx``, to represent the :class:`.Context`.
+ """
+ return True
+
+ def cog_command_error(self, ctx, error):
+ """A special method that is called whenever an error
+ is dispatched inside this cog.
+
+ This is similar to :func:`.on_command_error` except only applying
+ to the commands inside this cog.
+
+ This function **can** be a coroutine.
+
+ Parameters
+ -----------
+ ctx: :class:`.Context`
+ The invocation context where the error happened.
+ error: :class:`CommandError`
+ The error that happened.
+ """
+ pass
+
+ async def cog_before_invoke(self, ctx):
+ """A special method that acts as a cog local pre-invoke hook.
+
+ This is similar to :meth:`.Command.before_invoke`.
+
+ This **must** be a coroutine.
+
+ Parameters
+ -----------
+ ctx: :class:`.Context`
+ The invocation context.
+ """
+ pass
+
+ async def cog_after_invoke(self, ctx):
+ """A special method that acts as a cog local post-invoke hook.
+
+ This is similar to :meth:`.Command.after_invoke`.
+
+ This **must** be a coroutine.
+
+ Parameters
+ -----------
+ ctx: :class:`.Context`
+ The invocation context.
+ """
+ pass
+
+ def _inject(self, bot):
+ cls = self.__class__
+
+ # realistically, the only thing that can cause loading errors
+ # is essentially just the command loading, which raises if there are
+ # duplicates. When this condition is met, we want to undo all what
+ # we've added so far for some form of atomic loading.
+ for index, command in enumerate(self.__cog_commands__):
+ command.cog = self
+ if command.parent is None:
+ try:
+ bot.add_command(command)
+ except Exception as e:
+ # undo our additions
+ for to_undo in self.__cog_commands__[:index]:
+ bot.remove_command(to_undo)
+ raise e
+
+ # check if we're overriding the default
+ if cls.bot_check is not Cog.bot_check:
+ bot.add_check(self.bot_check)
+
+ if cls.bot_check_once is not Cog.bot_check_once:
+ bot.add_check(self.bot_check_once, call_once=True)
+
+ # while Bot.add_listener can raise if it's not a coroutine,
+ # this precondition is already met by the listener decorator
+ # already, thus this should never raise.
+ # Outside of, memory errors and the like...
+ for name, method_name in self.__cog_listeners__:
+ bot.add_listener(getattr(self, method_name), name)
+
+ return self
+
+ def _eject(self, bot):
+ cls = self.__class__
+
+ try:
+ for command in self.__cog_commands__:
+ if command.parent is None:
+ bot.remove_command(command.name)
+
+ for _, method_name in self.__cog_listeners__:
+ bot.remove_listener(getattr(self, method_name))
+
+ if cls.bot_check is not Cog.bot_check:
+ bot.remove_check(self.bot_check)
+
+ if cls.bot_check_once is not Cog.bot_check_once:
+ bot.remove_check(self.bot_check_once)
+ finally:
+ self.cog_unload()