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 | |
| 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')
| -rw-r--r-- | discord/ext/commands/__init__.py | 1 | ||||
| -rw-r--r-- | discord/ext/commands/_types.py | 30 | ||||
| -rw-r--r-- | discord/ext/commands/bot.py | 97 | ||||
| -rw-r--r-- | discord/ext/commands/cog.py | 325 | ||||
| -rw-r--r-- | discord/ext/commands/context.py | 6 | ||||
| -rw-r--r-- | discord/ext/commands/core.py | 197 | ||||
| -rw-r--r-- | discord/ext/commands/formatter.py | 2 |
7 files changed, 500 insertions, 158 deletions
diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py index 20749b21..b8195c62 100644 --- a/discord/ext/commands/__init__.py +++ b/discord/ext/commands/__init__.py @@ -17,3 +17,4 @@ from .errors import * from .formatter import HelpFormatter, Paginator from .converter import * from .cooldowns import * +from .cog import * diff --git a/discord/ext/commands/_types.py b/discord/ext/commands/_types.py new file mode 100644 index 00000000..667b1cba --- /dev/null +++ b/discord/ext/commands/_types.py @@ -0,0 +1,30 @@ +# -*- 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. +""" + +# This is merely a tag type to avoid circular import issues. +# Yes, this is a terrible solution but ultimately it is the only solution. +class _BaseCommand: + __slots__ = () diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 929e33cd..e0de7d4f 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -39,6 +39,7 @@ from .view import StringView from .context import Context from .errors import CommandNotFound, CommandError from .formatter import HelpFormatter +from .cog import Cog def when_mentioned(bot, msg): """A callable that implements a command prefix equivalent to being mentioned. @@ -181,7 +182,8 @@ class BotBase(GroupMixin): self.formatter = HelpFormatter() # pay no mind to this ugliness. - self.command(**self.help_attrs)(_default_help_command) + help_cmd = Command(_default_help_command, **self.help_attrs) + self.add_command(help_cmd) # internal helpers @@ -524,49 +526,24 @@ class BotBase(GroupMixin): 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. - - The cog can also have a ``__global_check`` member function that allows - you to define a global check. See :meth:`.check` for more info. If - the name is ``__global_check_once`` then it's equivalent to the - :meth:`.check_once` decorator. - - More information will be documented soon. - Parameters ----------- cog The cog to register to the bot. - """ - - self.cogs[type(cog).__name__] = cog - - try: - check = getattr(cog, '_{.__class__.__name__}__global_check'.format(cog)) - except AttributeError: - pass - else: - self.add_check(check) - try: - check = getattr(cog, '_{.__class__.__name__}__global_check_once'.format(cog)) - except AttributeError: - pass - else: - self.add_check(check, call_once=True) + Raises + ------- + TypeError + The cog does not inherit from :class:`.Cog`. + CommandError + An error happened during loading. + """ - members = inspect.getmembers(cog) - for name, member in members: - # register commands the cog has - if isinstance(member, Command): - if member.parent is None: - self.add_command(member) - continue + if not isinstance(cog, Cog): + raise TypeError('cogs must derive from Cog') - # register event listeners the cog has - if name.startswith('on_'): - self.add_listener(member, name) + cog = cog._inject(self) + self.cogs[cog.__cog_name__] = cog def get_cog(self, name): """Gets the cog instance requested. @@ -577,6 +554,8 @@ class BotBase(GroupMixin): ----------- name : str The name of the cog you are requesting. + This is equivalent to the name passed via keyword + argument in class creation or the class name if unspecified. """ return self.cogs.get(name) @@ -603,7 +582,7 @@ class BotBase(GroupMixin): except KeyError: return set() - return {c for c in self.all_commands.values() if c.instance is cog} + return {c for c in self.all_commands.values() if c.cog is cog} def remove_cog(self, name): """Removes a cog from the bot. @@ -613,13 +592,9 @@ class BotBase(GroupMixin): If no cog is found then this method has no effect. - If the cog defines a special member function named ``__unload`` - then it is called when removal has completed. This function - **cannot** be a coroutine. It must be a regular function. - Parameters ----------- - name : str + name: :class:`str` The name of the cog to remove. """ @@ -627,41 +602,7 @@ class BotBase(GroupMixin): if cog is None: return - members = inspect.getmembers(cog) - for name, member in members: - # remove commands the cog has - if isinstance(member, Command): - 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) - - try: - check = getattr(cog, '_{0.__class__.__name__}__global_check'.format(cog)) - except AttributeError: - pass - else: - self.remove_check(check) - - try: - check = getattr(cog, '_{0.__class__.__name__}__global_check_once'.format(cog)) - except AttributeError: - pass - else: - self.remove_check(check, call_once=True) - - unloader_name = '_{0.__class__.__name__}__unload'.format(cog) - try: - unloader = getattr(cog, unloader_name) - except AttributeError: - pass - else: - unloader() - - del cog + cog._eject(self) # extensions 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() diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index ea193696..bef94f04 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -118,8 +118,8 @@ class Context(discord.abc.Messageable): raise TypeError('Missing command to invoke.') from None arguments = [] - if command.instance is not None: - arguments.append(command.instance) + if command.cog is not None: + arguments.append(command.cog) arguments.append(self) arguments.extend(args[1:]) @@ -195,7 +195,7 @@ class Context(discord.abc.Messageable): if self.command is None: return None - return self.command.instance + return self.command.cog @discord.utils.cached_property def guild(self): diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 419594ac..4ac010e5 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -35,6 +35,8 @@ from .errors import * from .cooldowns import Cooldown, BucketType, CooldownMapping from .view import quoted_word from . import converter as converters +from ._types import _BaseCommand +from .cog import Cog __all__ = ['Command', 'Group', 'GroupMixin', 'command', 'group', 'has_role', 'has_permissions', 'has_any_role', 'check', @@ -102,7 +104,7 @@ class _CaseInsensitiveDict(dict): def __setitem__(self, k, v): super().__setitem__(k.lower(), v) -class Command: +class Command(_BaseCommand): r"""A class that implements the protocol for a bot text command. These are not created manually, instead they are created via the @@ -156,14 +158,44 @@ class Command: and ``b``). Otherwise :func:`.on_command_error` and local error handlers are called with :exc:`.TooManyArguments`. Defaults to ``True``. """ - def __init__(self, name, callback, **kwargs): - self.name = name + + def __new__(cls, *args, **kwargs): + # if you're wondering why this is done, it's because we need to ensure + # we have a complete original copy of **kwargs even for classes that + # mess with it by popping before delegating to the subclass __init__. + # In order to do this, we need to control the instance creation and + # inject the original kwargs through __new__ rather than doing it + # inside __init__. + self = super().__new__(cls) + + # we do a shallow copy because it's probably the most common use case. + # this could potentially break if someone modifies a list or something + # while it's in movement, but for now this is the cheapest and + # fastest way to do what we want. + self.__original_kwargs__ = kwargs.copy() + return self + + def __init__(self, func, **kwargs): + if not asyncio.iscoroutinefunction(func): + raise TypeError('Callback must be a coroutine.') + + self.name = name = kwargs.get('name') or func.__name__ if not isinstance(name, str): raise TypeError('Name of a command must be a string.') - self.callback = callback + self.callback = func self.enabled = kwargs.get('enabled', True) - self.help = kwargs.get('help') + + help_doc = kwargs.get('help') + if help_doc is not None: + help_doc = inspect.cleandoc(help_doc) + else: + help_doc = inspect.getdoc(func) + if isinstance(help_doc, bytes): + help_doc = help_doc.decode('utf-8') + + self.help = help_doc + self.brief = kwargs.get('brief') self.usage = kwargs.get('usage') self.rest_is_raw = kwargs.get('rest_is_raw', False) @@ -175,11 +207,29 @@ class Command: self.description = inspect.cleandoc(kwargs.get('description', '')) self.hidden = kwargs.get('hidden', False) - self.checks = kwargs.get('checks', []) + try: + checks = func.__commands_checks__ + checks.reverse() + del func.__commands_checks__ + except AttributeError: + checks = kwargs.get('checks', []) + finally: + self.checks = checks + + try: + cooldown = func.__commands_cooldown__ + del func.__commands_cooldown__ + except AttributeError: + cooldown = kwargs.get('cooldown') + finally: + self._buckets = CooldownMapping(cooldown) + self.ignore_extra = kwargs.get('ignore_extra', True) - self.instance = None - self.parent = None - self._buckets = CooldownMapping(kwargs.get('cooldown')) + self.cog = None + + # bandaid for the fact that sometimes parent can be the bot instance + parent = kwargs.get('parent') + self.parent = parent if isinstance(parent, _BaseCommand) else None self._before_invoke = None self._after_invoke = None @@ -206,9 +256,33 @@ class Command: if value.annotation is converters.Greedy: raise TypeError('Unparameterized Greedy[...] is disallowed in signature.') + def update(self, **kwargs): + """Updates :class:`Command` instance with updated attribute. + + This works similarly to the :func:`.command` decorator in terms + of parameters in that they are passed to the :class:`Command` or + subclass constructors, sans the name and callback. + """ + self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) + + def copy(self): + """Creates a copy of this :class:`Command`.""" + ret = self.__class__(self.callback, **self.__original_kwargs__) + ret._before_invoke = self._before_invoke + ret._after_invoke = self._after_invoke + return ret + + def _update_copy(self, kwargs): + if kwargs: + copy = self.__class__(self.callback, **kwargs) + copy.update(**self.__original_kwargs__) + return copy + else: + return self.copy() + async def dispatch_error(self, ctx, error): ctx.command_failed = True - cog = self.instance + cog = self.cog try: coro = self.on_error except AttributeError: @@ -221,20 +295,14 @@ class Command: await injected(ctx, error) try: - local = getattr(cog, '_{0.__class__.__name__}__error'.format(cog)) - except AttributeError: - pass - else: - wrapped = wrap_callback(local) - await wrapped(ctx, error) + if cog is not None: + local = Cog._get_overridden_method(cog.cog_command_error) + if local is not None: + wrapped = wrap_callback(local) + await wrapped(ctx, error) finally: ctx.bot.dispatch('command_error', ctx, error) - def __get__(self, instance, owner): - if instance is not None: - self.instance = instance - return self - async def _actual_conversion(self, ctx, converter, argument, param): if converter is bool: return _convert_to_bool(argument) @@ -392,7 +460,7 @@ class Command: Useful for inspecting signature. """ result = self.params.copy() - if self.instance is not None: + if self.cog is not None: # first parameter is self result.popitem(last=False) @@ -458,7 +526,7 @@ class Command: return self.qualified_name async def _parse_arguments(self, ctx): - ctx.args = [ctx] if self.instance is None else [self.instance, ctx] + ctx.args = [ctx] if self.cog is None else [self.cog, ctx] ctx.kwargs = {} args = ctx.args kwargs = ctx.kwargs @@ -466,7 +534,7 @@ class Command: view = ctx.view iterator = iter(self.params.items()) - if self.instance is not None: + if self.cog is not None: # we have 'self' as the first parameter so just advance # the iterator and resume parsing try: @@ -517,7 +585,7 @@ class Command: async def call_before_hooks(self, ctx): # now that we're done preparing we can call the pre-command hooks # first, call the command local hook: - cog = self.instance + cog = self.cog if self._before_invoke is not None: if cog is None: await self._before_invoke(ctx) @@ -525,12 +593,10 @@ class Command: await self._before_invoke(cog, ctx) # call the cog local hook if applicable: - try: - hook = getattr(cog, '_{0.__class__.__name__}__before_invoke'.format(cog)) - except AttributeError: - pass - else: - await hook(ctx) + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_before_invoke) + if hook is not None: + await hook(ctx) # call the bot global hook if necessary hook = ctx.bot._before_invoke @@ -538,19 +604,18 @@ class Command: await hook(ctx) async def call_after_hooks(self, ctx): - cog = self.instance + cog = self.cog if self._after_invoke is not None: if cog is None: await self._after_invoke(ctx) else: await self._after_invoke(cog, ctx) - try: - hook = getattr(cog, '_{0.__class__.__name__}__after_invoke'.format(cog)) - except AttributeError: - pass - else: - await hook(ctx) + # call the cog local hook if applicable: + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_after_invoke) + if hook is not None: + await hook(ctx) hook = ctx.bot._after_invoke if hook is not None: @@ -708,7 +773,7 @@ class Command: @property def cog_name(self): """The name of the cog this command belongs to. None otherwise.""" - return type(self.instance).__name__ if self.instance is not None else None + return type(self.cog).__cog_name__ if self.cog is not None else None @property def short_doc(self): @@ -793,13 +858,10 @@ class Command: if not await ctx.bot.can_run(ctx): raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self)) - cog = self.instance + cog = self.cog if cog is not None: - try: - local_check = getattr(cog, '_{0.__class__.__name__}__local_check'.format(cog)) - except AttributeError: - pass - else: + local_check = Cog._get_overridden_method(cog.cog_check) + if local_check is not None: ret = await discord.utils.maybe_coroutine(local_check, ctx) if not ret: return False @@ -825,11 +887,11 @@ class GroupMixin: case_insensitive: :class:`bool` Whether the commands should be case insensitive. Defaults to ``False``. """ - def __init__(self, **kwargs): + def __init__(self, *args, **kwargs): case_insensitive = kwargs.get('case_insensitive', False) self.all_commands = _CaseInsensitiveDict() if case_insensitive else {} self.case_insensitive = case_insensitive - super().__init__(**kwargs) + super().__init__(*args, **kwargs) @property def commands(self): @@ -955,6 +1017,7 @@ class GroupMixin: the internal command list via :meth:`~.GroupMixin.add_command`. """ def decorator(func): + kwargs.setdefault('parent', self) result = command(*args, **kwargs)(func) self.add_command(result) return result @@ -966,6 +1029,7 @@ class GroupMixin: the internal command list via :meth:`~.GroupMixin.add_command`. """ def decorator(func): + kwargs.setdefault('parent', self) result = group(*args, **kwargs)(func) self.add_command(result) return result @@ -994,9 +1058,16 @@ class Group(GroupMixin, Command): Indicates if the group's commands should be case insensitive. Defaults to ``False``. """ - def __init__(self, **attrs): + def __init__(self, *args, **attrs): self.invoke_without_command = attrs.pop('invoke_without_command', False) - super().__init__(**attrs) + super().__init__(*args, **attrs) + + def copy(self): + """Creates a copy of this :class:`Group`.""" + ret = super().copy() + for cmd in self.commands: + ret.add_command(cmd.copy()) + return ret async def invoke(self, ctx): early_invoke = not self.invoke_without_command @@ -1100,33 +1171,7 @@ def command(name=None, cls=None, **attrs): def decorator(func): if isinstance(func, Command): raise TypeError('Callback is already a command.') - if not asyncio.iscoroutinefunction(func): - raise TypeError('Callback must be a coroutine.') - - try: - checks = func.__commands_checks__ - checks.reverse() - del func.__commands_checks__ - except AttributeError: - checks = [] - - try: - cooldown = func.__commands_cooldown__ - del func.__commands_cooldown__ - except AttributeError: - cooldown = None - - help_doc = attrs.get('help') - if help_doc is not None: - help_doc = inspect.cleandoc(help_doc) - else: - help_doc = inspect.getdoc(func) - if isinstance(help_doc, bytes): - help_doc = help_doc.decode('utf-8') - - attrs['help'] = help_doc - fname = name or func.__name__ - return cls(name=fname, callback=func, checks=checks, cooldown=cooldown, **attrs) + return cls(func, name=name, **attrs) return decorator diff --git a/discord/ext/commands/formatter.py b/discord/ext/commands/formatter.py index d26e1f82..d9d61576 100644 --- a/discord/ext/commands/formatter.py +++ b/discord/ext/commands/formatter.py @@ -225,7 +225,7 @@ class HelpFormatter: cmd = tup[1] if self.is_cog(): # filter commands that don't exist to this cog. - if cmd.instance is not self.command: + if cmd.cog is not self.command: return False if cmd.hidden and not self.show_hidden: |