Rework entire cog system and partially document it and extensions.
This commit is contained in:
@ -17,3 +17,4 @@ from .errors import *
|
||||
from .formatter import HelpFormatter, Paginator
|
||||
from .converter import *
|
||||
from .cooldowns import *
|
||||
from .cog import *
|
||||
|
30
discord/ext/commands/_types.py
Normal file
30
discord/ext/commands/_types.py
Normal file
@ -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__ = ()
|
@ -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.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The cog does not inherit from :class:`.Cog`.
|
||||
CommandError
|
||||
An error happened during loading.
|
||||
"""
|
||||
|
||||
self.cogs[type(cog).__name__] = cog
|
||||
if not isinstance(cog, Cog):
|
||||
raise TypeError('cogs must derive from 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)
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
|
325
discord/ext/commands/cog.py
Normal file
325
discord/ext/commands/cog.py
Normal file
@ -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()
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
Reference in New Issue
Block a user