diff options
| author | Rapptz <[email protected]> | 2019-02-23 04:10:10 -0500 |
|---|---|---|
| committer | Rapptz <[email protected]> | 2019-02-23 04:10:10 -0500 |
| commit | caf3d17d4aa3ce45435bb96ea6d99317bfef8618 (patch) | |
| tree | d8bff0494efd7ba51d011ab4b7a9d30009eeae6d /discord/ext/commands/cog.py | |
| parent | [commands] Fix up wording on HelpFormatter.get_ending_note (diff) | |
| download | discord.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.py | 325 |
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() |