aboutsummaryrefslogtreecommitdiff
path: root/discord
diff options
context:
space:
mode:
Diffstat (limited to 'discord')
-rw-r--r--discord/ext/commands/__init__.py1
-rw-r--r--discord/ext/commands/_types.py30
-rw-r--r--discord/ext/commands/bot.py97
-rw-r--r--discord/ext/commands/cog.py325
-rw-r--r--discord/ext/commands/context.py6
-rw-r--r--discord/ext/commands/core.py197
-rw-r--r--discord/ext/commands/formatter.py2
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: