conflict fixes

This commit is contained in:
iDutchy
2021-04-07 18:27:34 -05:00
94 changed files with 4512 additions and 3018 deletions

View File

@ -1,17 +1,15 @@
# -*- coding: utf-8 -*-
"""
discord.ext.commands
~~~~~~~~~~~~~~~~~~~~~
An extension module to facilitate creation of bot commands.
:copyright: (c) 2015-2020 Rapptz
:copyright: (c) 2015-present Rapptz
:license: MIT, see LICENSE for more details.
"""
from .bot import Bot, AutoShardedBot, when_mentioned, when_mentioned_or
from .context import Context
from .bot import *
from .context import *
from .core import *
from .errors import *
from .help import *

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -42,12 +40,19 @@ from . import errors
from .help import HelpCommand, DefaultHelpCommand
from .cog import Cog
__all__ = (
'when_mentioned',
'when_mentioned_or',
'Bot',
'AutoShardedBot',
)
def when_mentioned(bot, msg):
"""A callable that implements a command prefix equivalent to being mentioned.
These are meant to be passed into the :attr:`.Bot.command_prefix` attribute.
"""
return [bot.user.mention + ' ', '<@!%s> ' % bot.user.id]
return [f'<@{bot.user.id}> ', f'<@!{bot.user.id}> ']
def when_mentioned_or(*prefixes):
"""A callable that implements when mentioned or other prefixes provided.
@ -110,12 +115,13 @@ class BotBase(GroupMixin):
self.description = inspect.cleandoc(description) if description else ''
self.owner_id = options.get('owner_id')
self.owner_ids = options.get('owner_ids', set())
self.strip_after_prefix = options.get('strip_after_prefix', False)
if self.owner_id and self.owner_ids:
raise TypeError('Both owner_id and owner_ids are set.')
if self.owner_ids and not isinstance(self.owner_ids, collections.abc.Collection):
raise TypeError('owner_ids must be a collection not {0.__class__!r}'.format(self.owner_ids))
raise TypeError(f'owner_ids must be a collection not {self.owner_ids.__class__!r}')
if options.pop('self_bot', False):
self._skip_check = lambda x, y: x != y
@ -190,11 +196,10 @@ class BotBase(GroupMixin):
return
cog = context.cog
if cog:
if Cog._get_overridden_method(cog.cog_command_error) is not None:
return
if cog and Cog._get_overridden_method(cog.cog_command_error) is not None:
return
print('Ignoring exception in command {}:'.format(context.command), file=sys.stderr)
print(f'Ignoring exception in command {context.command}:', file=sys.stderr)
traceback.print_exception(type(exception), exception, exception.__traceback__, file=sys.stderr)
# global check registration
@ -656,7 +661,13 @@ class BotBase(GroupMixin):
else:
self.__extensions[key] = lib
def load_extension(self, name):
def _resolve_name(self, name, package):
try:
return importlib.util.resolve_name(name, package)
except ImportError:
raise errors.ExtensionNotFound(name)
def load_extension(self, name, *, package=None):
"""Loads an extension.
An extension is a python module that contains commands, cogs, or
@ -672,11 +683,19 @@ class BotBase(GroupMixin):
The extension name to load. It must be dot separated like
regular Python imports if accessing a sub-module. e.g.
``foo.test`` if you want to import ``foo/test.py``.
package: Optional[:class:`str`]
The package name to resolve relative imports with.
This is required when loading an extension using a relative path, e.g ``.foo.test``.
Defaults to ``None``.
.. versionadded:: 1.7
Raises
--------
ExtensionNotFound
The extension could not be imported.
This is also raised if the name of the extension could not
be resolved using the provided ``package`` parameter.
ExtensionAlreadyLoaded
The extension is already loaded.
NoEntryPointError
@ -685,6 +704,7 @@ class BotBase(GroupMixin):
The extension or its setup function had an execution error.
"""
name = self._resolve_name(name, package)
if name in self.__extensions:
raise errors.ExtensionAlreadyLoaded(name)
@ -694,7 +714,7 @@ class BotBase(GroupMixin):
self._load_from_module_spec(spec, name)
def unload_extension(self, name):
def unload_extension(self, name, *, package=None):
"""Unloads an extension.
When the extension is unloaded, all commands, listeners, and cogs are
@ -711,13 +731,23 @@ class BotBase(GroupMixin):
The extension name to unload. It must be dot separated like
regular Python imports if accessing a sub-module. e.g.
``foo.test`` if you want to import ``foo/test.py``.
package: Optional[:class:`str`]
The package name to resolve relative imports with.
This is required when unloading an extension using a relative path, e.g ``.foo.test``.
Defaults to ``None``.
.. versionadded:: 1.7
Raises
-------
ExtensionNotFound
The name of the extension could not
be resolved using the provided ``package`` parameter.
ExtensionNotLoaded
The extension was not loaded.
"""
name = self._resolve_name(name, package)
lib = self.__extensions.get(name)
if lib is None:
raise errors.ExtensionNotLoaded(name)
@ -725,7 +755,7 @@ class BotBase(GroupMixin):
self._remove_module_references(lib.__name__)
self._call_module_finalizers(lib, name)
def reload_extension(self, name):
def reload_extension(self, name, *, package=None):
"""Atomically reloads an extension.
This replaces the extension with the same extension, only refreshed. This is
@ -739,6 +769,12 @@ class BotBase(GroupMixin):
The extension name to reload. It must be dot separated like
regular Python imports if accessing a sub-module. e.g.
``foo.test`` if you want to import ``foo/test.py``.
package: Optional[:class:`str`]
The package name to resolve relative imports with.
This is required when reloading an extension using a relative path, e.g ``.foo.test``.
Defaults to ``None``.
.. versionadded:: 1.7
Raises
-------
@ -746,12 +782,15 @@ class BotBase(GroupMixin):
The extension was not loaded.
ExtensionNotFound
The extension could not be imported.
This is also raised if the name of the extension could not
be resolved using the provided ``package`` parameter.
NoEntryPointError
The extension does not have a setup function.
ExtensionFailed
The extension setup function had an execution error.
"""
name = self._resolve_name(name, package)
lib = self.__extensions.get(name)
if lib is None:
raise errors.ExtensionNotLoaded(name)
@ -768,7 +807,7 @@ class BotBase(GroupMixin):
self._remove_module_references(lib.__name__)
self._call_module_finalizers(lib, name)
self.load_extension(name)
except Exception as e:
except Exception:
# if the load failed, the remnants should have been
# cleaned from the load_extension function call
# so let's load it from our old compiled library.
@ -920,6 +959,9 @@ class BotBase(GroupMixin):
# Getting here shouldn't happen
raise
if self.strip_after_prefix:
view.skip_ws()
invoker = view.get_word()
ctx.invoked_with = invoker
ctx.prefix = invoked_prefix
@ -949,7 +991,7 @@ class BotBase(GroupMixin):
else:
self.dispatch('command_completion', ctx)
elif ctx.invoked_with:
exc = errors.CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with))
exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found')
self.dispatch('command_error', ctx, exc)
async def process_commands(self, message):
@ -1054,6 +1096,12 @@ class Bot(BotBase, discord.Client):
for the collection. You cannot set both ``owner_id`` and ``owner_ids``.
.. versionadded:: 1.3
strip_after_prefix: :class:`bool`
Whether to strip whitespace characters after encountering the command
prefix. This allows for ``! hello`` and ``!hello`` to both work if
the ``command_prefix`` is set to ``!``. Defaults to ``False``.
.. versionadded:: 1.7
"""
pass

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -101,7 +99,7 @@ class CogMeta(type):
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', {})
attrs['__cog_settings__'] = kwargs.pop('command_attrs', {})
aliases = kwargs.pop('aliases', [])
if not isinstance(aliases, list):
@ -130,13 +128,13 @@ class CogMeta(type):
value = value.__func__
if isinstance(value, _BaseCommand):
if is_static_method:
raise TypeError('Command in method {0}.{1!r} must not be staticmethod.'.format(base, elem))
raise TypeError(f'Command in method {base}.{elem!r} must not be staticmethod.')
if elem.startswith(('cog_', 'bot_')):
raise TypeError(no_bot_cog.format(base, elem))
commands[elem] = value
elif inspect.iscoroutinefunction(value):
try:
is_listener = getattr(value, '__cog_listener__')
getattr(value, '__cog_listener__')
except AttributeError:
continue
else:
@ -202,7 +200,7 @@ class Cog(metaclass=CogMeta):
parent = lookup[parent.qualified_name]
# Update our parent's reference to our self
removed = parent.remove_command(command.name)
parent.remove_command(command.name)
parent.add_command(command)
return self
@ -285,7 +283,7 @@ class Cog(metaclass=CogMeta):
"""
if name is not None and not isinstance(name, str):
raise TypeError('Cog.listener expected str but received {0.__class__.__name__!r} instead.'.format(name))
raise TypeError(f'Cog.listener expected str but received {name.__class__.__name__!r} instead.')
def decorator(func):
actual = func
@ -306,6 +304,13 @@ class Cog(metaclass=CogMeta):
return func
return decorator
def has_error_handler(self):
""":class:`bool`: Checks whether the cog has an error handler.
.. versionadded:: 1.7
"""
return not hasattr(self.cog_command_error.__func__, '__cog_special_method__')
@_cog_special_method
def cog_unload(self):
"""A special method that is called when the cog gets removed.
@ -411,7 +416,8 @@ class Cog(metaclass=CogMeta):
except Exception as e:
# undo our additions
for to_undo in self.__cog_commands__[:index]:
bot.remove_command(to_undo.name)
if to_undo.parent is None:
bot.remove_command(to_undo.name)
raise e
# check if we're overriding the default

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -28,6 +26,10 @@ import re
import discord.abc
import discord.utils
__all__ = (
'Context',
)
class Context(discord.abc.Messageable):
r"""Represents the context in which a command is being invoked under.
@ -58,6 +60,14 @@ class Context(discord.abc.Messageable):
invoked_with: :class:`str`
The command name that triggered this invocation. Useful for finding out
which alias called the command.
invoked_parents: List[:class:`str`]
The command names of the parents that triggered this invocation. Useful for
finding out which aliases called the command.
For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``.
.. versionadded:: 1.7
invoked_subcommand: :class:`Command`
The subcommand that was invoked.
If no valid subcommand was invoked then this is equal to ``None``.
@ -80,6 +90,7 @@ class Context(discord.abc.Messageable):
self.command = attrs.pop('command', None)
self.view = attrs.pop('view', None)
self.invoked_with = attrs.pop('invoked_with', None)
self.invoked_parents = attrs.pop('invoked_parents', [])
self.invoked_subcommand = attrs.pop('invoked_subcommand', None)
self.subcommand_passed = attrs.pop('subcommand_passed', None)
self.command_failed = attrs.pop('command_failed', False)
@ -94,7 +105,7 @@ class Context(discord.abc.Messageable):
pattern = re.compile(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.prefix)
async def invoke(self, *args, **kwargs):
async def invoke(self, command, /, *args, **kwargs):
r"""|coro|
Calls a command with the arguments given.
@ -111,10 +122,6 @@ class Context(discord.abc.Messageable):
You must take care in passing the proper arguments when
using this function.
.. warning::
The first parameter passed **must** be the command being invoked.
Parameters
-----------
command: :class:`.Command`
@ -129,18 +136,12 @@ class Context(discord.abc.Messageable):
TypeError
The command argument to invoke is missing.
"""
try:
command = args[0]
except IndexError:
raise TypeError('Missing command to invoke.') from None
arguments = []
if command.cog is not None:
arguments.append(command.cog)
arguments.append(self)
arguments.extend(args[1:])
arguments.extend(args)
ret = await command.callback(*arguments, **kwargs)
return ret
@ -184,13 +185,15 @@ class Context(discord.abc.Messageable):
index, previous = view.index, view.previous
invoked_with = self.invoked_with
invoked_subcommand = self.invoked_subcommand
invoked_parents = self.invoked_parents
subcommand_passed = self.subcommand_passed
if restart:
to_call = cmd.root_parent or cmd
view.index = len(self.prefix)
view.previous = 0
view.get_word() # advance to get the root command
self.invoked_parents = []
self.invoked_with = view.get_word() # advance to get the root command
else:
to_call = cmd
@ -202,6 +205,7 @@ class Context(discord.abc.Messageable):
view.previous = previous
self.invoked_with = invoked_with
self.invoked_subcommand = invoked_subcommand
self.invoked_parents = invoked_parents
self.subcommand_passed = subcommand_passed
@property
@ -214,7 +218,7 @@ class Context(discord.abc.Messageable):
@property
def cog(self):
""":class:`.Cog`: Returns the cog associated with this context's command. None if it does not exist."""
"""Optional[:class:`.Cog`]: Returns the cog associated with this context's command. None if it does not exist."""
if self.command is None:
return None
@ -227,8 +231,8 @@ class Context(discord.abc.Messageable):
@discord.utils.cached_property
def channel(self):
""":class:`.TextChannel`:
Returns the channel associated with this context's command. Shorthand for :attr:`.Message.channel`.
"""Union[:class:`.abc.Messageable`]: Returns the channel associated with this context's command.
Shorthand for :attr:`.Message.channel`.
"""
return self.message.channel
@ -311,7 +315,7 @@ class Context(discord.abc.Messageable):
entity = bot.get_cog(entity) or bot.get_command(entity)
try:
qualified_name = entity.qualified_name
entity.qualified_name
except AttributeError:
# if we're here then it's not a cog, group, or command.
return None
@ -333,7 +337,6 @@ class Context(discord.abc.Messageable):
except CommandError as e:
await cmd.on_help_command_error(self, e)
@discord.utils.copy_doc(discord.Message.reply)
async def reply(self, content=None, **kwargs):
return await self.message.reply(content, **kwargs)
reply.__doc__ = discord.Message.reply.__doc__

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -24,34 +22,44 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import re
import inspect
import typing
from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, Union, runtime_checkable
import discord
from .errors import *
if TYPE_CHECKING:
from .context import Context
__all__ = (
'Converter',
'MemberConverter',
'UserConverter',
'MessageConverter',
'PartialMessageConverter',
'TextChannelConverter',
'InviteConverter',
'GuildConverter',
'RoleConverter',
'GameConverter',
'ColourConverter',
'ColorConverter',
'VoiceChannelConverter',
'StageChannelConverter',
'EmojiConverter',
'PartialEmojiConverter',
'CategoryChannelConverter',
'IDConverter',
'StoreChannelConverter',
'clean_content',
'Greedy',
)
def _get_from_guilds(bot, getter, argument):
result = None
for guild in bot.guilds:
@ -60,9 +68,13 @@ def _get_from_guilds(bot, getter, argument):
return result
return result
_utils_get = discord.utils.get
class Converter:
_utils_get = discord.utils.get
T = TypeVar('T', covariant=True)
@runtime_checkable
class Converter(Protocol[T]):
"""The base class of custom converters that require the :class:`.Context`
to be passed to be useful.
@ -73,7 +85,7 @@ class Converter:
method to do its conversion logic. This method must be a :ref:`coroutine <coroutine>`.
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> T:
"""|coro|
The method to override to do conversion logic.
@ -98,15 +110,15 @@ class Converter:
"""
raise NotImplementedError('Derived classes need to implement this.')
class IDConverter(Converter):
class IDConverter(Converter[T]):
def __init__(self):
self._id_regex = re.compile(r'([0-9]{15,21})$')
self._id_regex = re.compile(r'([0-9]{15,20})$')
super().__init__()
def _get_id_match(self, argument):
return self._id_regex.match(argument)
class MemberConverter(IDConverter):
class MemberConverter(IDConverter[discord.Member]):
"""Converts to a :class:`~discord.Member`.
All lookups are via the local guild. If in a DM context, then the lookup
@ -159,7 +171,7 @@ class MemberConverter(IDConverter):
return None
return members[0]
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.Member:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
guild = ctx.guild
@ -192,7 +204,7 @@ class MemberConverter(IDConverter):
return result
class UserConverter(IDConverter):
class UserConverter(IDConverter[discord.User]):
"""Converts to a :class:`~discord.User`.
All lookups are via the global user cache.
@ -211,7 +223,7 @@ class UserConverter(IDConverter):
This converter now lazily fetches users from the HTTP APIs if an ID is passed
and it's not available in cache.
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.User:
match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument)
result = None
state = ctx._state
@ -251,7 +263,39 @@ class UserConverter(IDConverter):
return result
class MessageConverter(Converter):
class PartialMessageConverter(Converter[discord.PartialMessage]):
"""Converts to a :class:`discord.PartialMessage`.
.. versionadded:: 1.7
The creation strategy is as follows (in order):
1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID")
2. By message ID (The message is assumed to be in the context channel.)
3. By message URL
"""
@staticmethod
def _get_id_matches(argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,20})-)?(?P<message_id>[0-9]{15,20})$')
link_regex = re.compile(
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
r'(?:[0-9]{15,20}|@me)'
r'/(?P<channel_id>[0-9]{15,20})/(?P<message_id>[0-9]{15,20})/?$'
)
match = id_regex.match(argument) or link_regex.match(argument)
if not match:
raise MessageNotFound(argument)
channel_id = match.group("channel_id")
return int(match.group("message_id")), int(channel_id) if channel_id else None
async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage:
message_id, channel_id = self._get_id_matches(argument)
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
if not channel:
raise ChannelNotFound(channel_id)
return discord.PartialMessage(channel=channel, id=message_id)
class MessageConverter(IDConverter[discord.Message]):
"""Converts to a :class:`discord.Message`.
.. versionadded:: 1.1
@ -265,22 +309,12 @@ class MessageConverter(Converter):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
id_regex = re.compile(r'(?:(?P<channel_id>[0-9]{15,21})-)?(?P<message_id>[0-9]{15,21})$')
link_regex = re.compile(
r'https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/'
r'(?:[0-9]{15,21}|@me)'
r'/(?P<channel_id>[0-9]{15,21})/(?P<message_id>[0-9]{15,21})/?$'
)
match = id_regex.match(argument) or link_regex.match(argument)
if not match:
raise MessageNotFound(argument)
message_id = int(match.group("message_id"))
channel_id = match.group("channel_id")
async def convert(self, ctx: Context, argument: str) -> discord.Message:
message_id, channel_id = PartialMessageConverter._get_id_matches(argument)
message = ctx.bot._connection._get_message(message_id)
if message:
return message
channel = ctx.bot.get_channel(int(channel_id)) if channel_id else ctx.channel
channel = ctx.bot.get_channel(channel_id) if channel_id else ctx.channel
if not channel:
raise ChannelNotFound(channel_id)
try:
@ -290,7 +324,7 @@ class MessageConverter(Converter):
except discord.Forbidden:
raise ChannelNotReadable(channel)
class TextChannelConverter(IDConverter):
class TextChannelConverter(IDConverter[discord.TextChannel]):
"""Converts to a :class:`~discord.TextChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
@ -305,7 +339,7 @@ class TextChannelConverter(IDConverter):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.TextChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
@ -332,7 +366,7 @@ class TextChannelConverter(IDConverter):
return result
class VoiceChannelConverter(IDConverter):
class VoiceChannelConverter(IDConverter[discord.VoiceChannel]):
"""Converts to a :class:`~discord.VoiceChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
@ -347,7 +381,7 @@ class VoiceChannelConverter(IDConverter):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
@ -373,7 +407,47 @@ class VoiceChannelConverter(IDConverter):
return result
class CategoryChannelConverter(IDConverter):
class StageChannelConverter(IDConverter[discord.StageChannel]):
"""Converts to a :class:`~discord.StageChannel`.
.. versionadded:: 1.7
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name
"""
async def convert(self, ctx: Context, argument: str) -> discord.StageChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.stage_channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StageChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StageChannel):
raise ChannelNotFound(argument)
return result
class CategoryChannelConverter(IDConverter[discord.CategoryChannel]):
"""Converts to a :class:`~discord.CategoryChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
@ -388,7 +462,7 @@ class CategoryChannelConverter(IDConverter):
.. versionchanged:: 1.5
Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
@ -415,7 +489,48 @@ class CategoryChannelConverter(IDConverter):
return result
class ColourConverter(Converter):
class StoreChannelConverter(IDConverter[discord.StoreChannel]):
"""Converts to a :class:`~discord.StoreChannel`.
All lookups are via the local guild. If in a DM context, then the lookup
is done by the global cache.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by mention.
3. Lookup by name.
.. versionadded:: 1.7
"""
async def convert(self, ctx: Context, argument: str) -> discord.StoreChannel:
bot = ctx.bot
match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument)
result = None
guild = ctx.guild
if match is None:
# not a mention
if guild:
result = discord.utils.get(guild.channels, name=argument)
else:
def check(c):
return isinstance(c, discord.StoreChannel) and c.name == argument
result = discord.utils.find(check, bot.get_all_channels())
else:
channel_id = int(match.group(1))
if guild:
result = guild.get_channel(channel_id)
else:
result = _get_from_guilds(bot, 'get_channel', channel_id)
if not isinstance(result, discord.StoreChannel):
raise ChannelNotFound(argument)
return result
class ColourConverter(Converter[discord.Colour]):
"""Converts to a :class:`~discord.Colour`.
.. versionchanged:: 1.5
@ -426,33 +541,80 @@ class ColourConverter(Converter):
- ``0x<hex>``
- ``#<hex>``
- ``0x#<hex>``
- ``rgb(<number>, <number>, <number>)``
- Any of the ``classmethod`` in :class:`Colour`
- The ``_`` in the name can be optionally replaced with spaces.
Like CSS, ``<number>`` can be either 0-255 or 0-100% and ``<hex>`` can be
either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff).
.. versionchanged:: 1.5
Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
arg = argument.replace('0x', '').lower()
if arg[0] == '#':
arg = arg[1:]
.. versionchanged:: 1.7
Added support for ``rgb`` function and 3-digit hex shortcuts
"""
RGB_REGEX = re.compile(r'rgb\s*\((?P<r>[0-9]{1,3}%?)\s*,\s*(?P<g>[0-9]{1,3}%?)\s*,\s*(?P<b>[0-9]{1,3}%?)\s*\)')
def parse_hex_number(self, argument):
arg = ''.join(i * 2 for i in argument) if len(argument) == 3 else argument
try:
value = int(arg, base=16)
if not (0 <= value <= 0xFFFFFF):
raise BadColourArgument(arg)
return discord.Colour(value=value)
raise BadColourArgument(argument)
except ValueError:
arg = arg.replace(' ', '_')
method = getattr(discord.Colour, arg, None)
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
raise BadColourArgument(argument)
else:
return discord.Color(value=value)
def parse_rgb_number(self, argument, number):
if number[-1] == '%':
value = int(number[:-1])
if not (0 <= value <= 100):
raise BadColourArgument(argument)
return round(255 * (value / 100))
value = int(number)
if not (0 <= value <= 255):
raise BadColourArgument(argument)
return value
def parse_rgb(self, argument, *, regex=RGB_REGEX):
match = regex.match(argument)
if match is None:
raise BadColourArgument(argument)
red = self.parse_rgb_number(argument, match.group('r'))
green = self.parse_rgb_number(argument, match.group('g'))
blue = self.parse_rgb_number(argument, match.group('b'))
return discord.Color.from_rgb(red, green, blue)
async def convert(self, ctx: Context, argument: str) -> discord.Colour:
if argument[0] == '#':
return self.parse_hex_number(argument[1:])
if argument[0:2] == '0x':
rest = argument[2:]
# Legacy backwards compatible syntax
if rest.startswith('#'):
return self.parse_hex_number(rest[1:])
return self.parse_hex_number(rest)
arg = argument.lower()
if arg[0:3] == 'rgb':
return self.parse_rgb(arg)
arg = arg.replace(' ', '_')
method = getattr(discord.Colour, arg, None)
if arg.startswith('from_') or method is None or not inspect.ismethod(method):
raise BadColourArgument(arg)
return method()
ColorConverter = ColourConverter
class RoleConverter(IDConverter):
class RoleConverter(IDConverter[discord.Role]):
"""Converts to a :class:`~discord.Role`.
All lookups are via the local guild. If in a DM context, then the lookup
@ -467,7 +629,7 @@ class RoleConverter(IDConverter):
.. versionchanged:: 1.5
Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.Role:
guild = ctx.guild
if not guild:
raise NoPrivateMessage()
@ -482,12 +644,12 @@ class RoleConverter(IDConverter):
raise RoleNotFound(argument)
return result
class GameConverter(Converter):
class GameConverter(Converter[discord.Game]):
"""Converts to :class:`~discord.Game`."""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.Game:
return discord.Game(name=argument)
class InviteConverter(Converter):
class InviteConverter(Converter[discord.Invite]):
"""Converts to a :class:`~discord.Invite`.
This is done via an HTTP request using :meth:`.Bot.fetch_invite`.
@ -495,14 +657,40 @@ class InviteConverter(Converter):
.. versionchanged:: 1.5
Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.Invite:
try:
invite = await ctx.bot.fetch_invite(argument)
return invite
except Exception as exc:
raise BadInviteArgument() from exc
class EmojiConverter(IDConverter):
class GuildConverter(IDConverter[discord.Guild]):
"""Converts to a :class:`~discord.Guild`.
The lookup strategy is as follows (in order):
1. Lookup by ID.
2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names).
.. versionadded:: 1.7
"""
async def convert(self, ctx: Context, argument: str) -> discord.Guild:
match = self._get_id_match(argument)
result = None
if match is not None:
guild_id = int(match.group(1))
result = ctx.bot.get_guild(guild_id)
if result is None:
result = discord.utils.get(ctx.bot.guilds, name=argument)
if result is None:
raise GuildNotFound(argument)
return result
class EmojiConverter(IDConverter[discord.Emoji]):
"""Converts to a :class:`~discord.Emoji`.
All lookups are done for the local guild first, if available. If that lookup
@ -517,7 +705,7 @@ class EmojiConverter(IDConverter):
.. versionchanged:: 1.5
Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.Emoji:
match = self._get_id_match(argument) or re.match(r'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', argument)
result = None
bot = ctx.bot
@ -545,7 +733,7 @@ class EmojiConverter(IDConverter):
return result
class PartialEmojiConverter(Converter):
class PartialEmojiConverter(Converter[discord.PartialEmoji]):
"""Converts to a :class:`~discord.PartialEmoji`.
This is done by extracting the animated flag, name and ID from the emoji.
@ -553,7 +741,7 @@ class PartialEmojiConverter(Converter):
.. versionchanged:: 1.5
Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument`
"""
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji:
match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument)
if match:
@ -566,7 +754,7 @@ class PartialEmojiConverter(Converter):
raise PartialEmojiConversionFailure(argument)
class clean_content(Converter):
class clean_content(Converter[str]):
"""Converts the argument to mention scrubbed version of
said content.
@ -580,20 +768,25 @@ class clean_content(Converter):
Whether to use nicknames when transforming mentions.
escape_markdown: :class:`bool`
Whether to also escape special markdown characters.
remove_markdown: :class:`bool`
Whether to also remove special markdown characters. This option is not supported with ``escape_markdown``
.. versionadded:: 1.7
"""
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False):
def __init__(self, *, fix_channel_mentions=False, use_nicknames=True, escape_markdown=False, remove_markdown=False):
self.fix_channel_mentions = fix_channel_mentions
self.use_nicknames = use_nicknames
self.escape_markdown = escape_markdown
self.remove_markdown = remove_markdown
async def convert(self, ctx, argument):
async def convert(self, ctx: Context, argument: str) -> str:
message = ctx.message
transformations = {}
if self.fix_channel_mentions and ctx.guild:
def resolve_channel(id, *, _get=ctx.guild.get_channel):
ch = _get(id)
return ('<#%s>' % id), ('#' + ch.name if ch else '#deleted-channel')
return f'<#{id}>', ('#' + ch.name if ch else '#deleted-channel')
transformations.update(resolve_channel(channel) for channel in message.raw_channel_mentions)
@ -608,12 +801,12 @@ class clean_content(Converter):
transformations.update(
('<@%s>' % member_id, resolve_member(member_id))
(f'<@{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions
)
transformations.update(
('<@!%s>' % member_id, resolve_member(member_id))
(f'<@!{member_id}>', resolve_member(member_id))
for member_id in message.raw_mentions
)
@ -623,7 +816,7 @@ class clean_content(Converter):
return '@' + r.name if r else '@deleted-role'
transformations.update(
('<@&%s>' % role_id, resolve_role(role_id))
(f'<@&{role_id}>', resolve_role(role_id))
for role_id in message.raw_role_mentions
)
@ -635,6 +828,8 @@ class clean_content(Converter):
if self.escape_markdown:
result = discord.utils.escape_markdown(result)
elif self.remove_markdown:
result = discord.utils.remove_markdown(result)
# Completely ensure no mentions escape:
return discord.utils.escape_mentions(result)
@ -656,10 +851,10 @@ class _Greedy:
raise TypeError('Greedy[...] expects a type or a Converter instance.')
if converter is str or converter is type(None) or converter is _Greedy:
raise TypeError('Greedy[%s] is invalid.' % converter.__name__)
raise TypeError(f'Greedy[{converter.__name__}] is invalid.')
if getattr(converter, '__origin__', None) is typing.Union and type(None) in converter.__args__:
raise TypeError('Greedy[%r] is invalid.' % converter)
if getattr(converter, '__origin__', None) is Union and type(None) in converter.__args__:
raise TypeError(f'Greedy[{converter!r}] is invalid.')
return self.__class__(converter=converter)

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -66,6 +64,9 @@ class BucketType(Enum):
# recieving a DMChannel or GroupChannel which inherit from PrivateChannel and do
return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id
def __call__(self, msg):
return self.get_key(msg)
class Cooldown:
__slots__ = ('rate', 'per', 'type', '_window', '_tokens', '_last')
@ -78,8 +79,8 @@ class Cooldown:
self._tokens = self.rate
self._last = 0.0
if not isinstance(self.type, BucketType):
raise TypeError('Cooldown type must be a BucketType')
if not callable(self.type):
raise TypeError('Cooldown type must be a BucketType or callable')
def get_tokens(self, current=None):
if not current:
@ -151,7 +152,7 @@ class CooldownMapping:
return cls(Cooldown(rate, per, type))
def _bucket_key(self, msg):
return self._cooldown.type.get_key(msg)
return self._cooldown.type(msg)
def _verify_cache_integrity(self, current=None):
# we want to delete all cache objects that haven't been used
@ -252,7 +253,7 @@ class MaxConcurrency:
raise ValueError('max_concurrency \'number\' cannot be less than 1')
if not isinstance(per, BucketType):
raise TypeError('max_concurrency \'per\' must be of type BucketType not %r' % type(per))
raise TypeError(f'max_concurrency \'per\' must be of type BucketType not {type(per)!r}')
def copy(self):
return self.__class__(self.number, per=self.per, wait=self.wait)

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -29,6 +27,7 @@ import functools
import inspect
import typing
import datetime
import sys
import discord
@ -158,7 +157,7 @@ class Command(_BaseCommand):
isn't one.
cog: Optional[:class:`Cog`]
The cog that this command belongs to. ``None`` if there isn't one.
checks: List[Callable[..., :class:`bool`]]
checks: List[Callable[[:class:`.Context`], :class:`bool`]]
A list of predicates that verifies if the command could be executed
with the given :class:`.Context` as the sole parameter. If an exception
is necessary to be thrown to signal failure, then one inherited from
@ -301,17 +300,37 @@ class Command(_BaseCommand):
signature = inspect.signature(function)
self.params = signature.parameters.copy()
# PEP-563 allows postponing evaluation of annotations with a __future__
# import. When postponed, Parameter.annotation will be a string and must
# be replaced with the real value for the converters to work later on
# see: https://bugs.python.org/issue41341
resolve = self._recursive_resolve if sys.version_info < (3, 9) else self._return_resolved
try:
type_hints = {k: resolve(v) for k, v in typing.get_type_hints(function).items()}
except NameError as e:
raise NameError(f'unresolved forward reference: {e.args[0]}') from None
for key, value in self.params.items():
if isinstance(value.annotation, str):
self.params[key] = value = value.replace(annotation=eval(value.annotation, function.__globals__))
# coalesce the forward references
if key in type_hints:
self.params[key] = value = value.replace(annotation=type_hints[key])
# fail early for when someone passes an unparameterized Greedy type
if value.annotation is converters.Greedy:
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
def _return_resolved(self, type, **kwargs):
return type
def _recursive_resolve(self, type, *, globals=None):
if not isinstance(type, typing.ForwardRef):
return type
resolved = eval(type.__forward_arg__, globals)
args = typing.get_args(resolved)
for index, arg in enumerate(args):
inner_resolve_result = self._recursive_resolve(arg, globals=globals)
resolved[index] = inner_resolve_result
return resolved
def add_check(self, func):
"""Adds a check to the command.
@ -445,19 +464,13 @@ class Command(_BaseCommand):
converter = getattr(converters, converter.__name__ + 'Converter', converter)
try:
if inspect.isclass(converter):
if issubclass(converter, converters.Converter):
instance = converter()
ret = await instance.convert(ctx, argument)
return ret
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
if inspect.ismethod(converter.convert):
return await converter.convert(ctx, argument)
else:
method = getattr(converter, 'convert', None)
if method is not None and inspect.ismethod(method):
ret = await method(ctx, argument)
return ret
return await converter().convert(ctx, argument)
elif isinstance(converter, converters.Converter):
ret = await converter.convert(ctx, argument)
return ret
return await converter.convert(ctx, argument)
except CommandError:
raise
except Exception as exc:
@ -473,7 +486,7 @@ class Command(_BaseCommand):
except AttributeError:
name = converter.__class__.__name__
raise BadArgument('Converting to "{}" failed for parameter "{}".'.format(name, param.name)) from exc
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
async def do_conversion(self, ctx, converter, argument, param):
try:
@ -523,7 +536,7 @@ class Command(_BaseCommand):
# The greedy converter is simple -- it keeps going until it fails in which case,
# it undos the view ready for the next parameter to use instead
if type(converter) is converters._Greedy:
if param.kind == param.POSITIONAL_OR_KEYWORD:
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
elif param.kind == param.VAR_POSITIONAL:
return await self._transform_greedy_var_pos(ctx, param, converter.converter)
@ -693,7 +706,7 @@ class Command(_BaseCommand):
raise discord.ClientException(fmt.format(self))
for name, param in iterator:
if param.kind == param.POSITIONAL_OR_KEYWORD:
if param.kind == param.POSITIONAL_OR_KEYWORD or param.kind == param.POSITIONAL_ONLY:
transformed = await self.transform(ctx, param)
args.append(transformed)
elif param.kind == param.KEYWORD_ONLY:
@ -715,9 +728,8 @@ class Command(_BaseCommand):
except RuntimeError:
break
if not self.ignore_extra:
if not view.eof:
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
if not self.ignore_extra and not view.eof:
raise TooManyArguments('Too many arguments passed to ' + self.qualified_name)
async def call_before_hooks(self, ctx):
# now that we're done preparing we can call the pre-command hooks
@ -776,7 +788,7 @@ class Command(_BaseCommand):
ctx.command = self
if not await self.can_run(ctx):
raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self))
raise CheckFailure(f'The check functions for command {self.qualified_name} failed.')
if self._max_concurrency is not None:
await self._max_concurrency.acquire(ctx)
@ -904,6 +916,13 @@ class Command(_BaseCommand):
self.on_error = coro
return coro
def has_error_handler(self):
""":class:`bool`: Checks whether the command has an error handler registered.
.. versionadded:: 1.7
"""
return hasattr(self, 'on_error')
def before_invoke(self, coro):
"""A decorator that registers a coroutine as a pre-invoke hook.
@ -1008,23 +1027,23 @@ class Command(_BaseCommand):
# do [name] since [name=None] or [name=] are not exactly useful for the user.
should_print = param.default if isinstance(param.default, str) else param.default is not None
if should_print:
result.append('[%s=%s]' % (name, param.default) if not greedy else
'[%s=%s]...' % (name, param.default))
result.append(f'[{name}={param.default}]' if not greedy else
f'[{name}={param.default}]...')
continue
else:
result.append('[%s]' % name)
result.append(f'[{name}]')
elif param.kind == param.VAR_POSITIONAL:
if self.require_var_positional:
result.append('<%s...>' % name)
result.append(f'<{name}...>')
else:
result.append('[%s...]' % name)
result.append(f'[{name}...]')
elif greedy:
result.append('[%s]...' % name)
result.append(f'[{name}]...')
elif self._is_typing_optional(param.annotation):
result.append('[%s]' % name)
result.append(f'[{name}]')
else:
result.append('<%s>' % name)
result.append(f'<{name}>')
return ' '.join(result)
@ -1056,14 +1075,14 @@ class Command(_BaseCommand):
"""
if not self.enabled:
raise DisabledCommand('{0.name} command is disabled'.format(self))
raise DisabledCommand(f'{self.name} command is disabled')
original = ctx.command
ctx.command = self
try:
if not await ctx.bot.can_run(ctx):
raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self))
raise CheckFailure(f'The global check functions for command {self.qualified_name} failed.')
cog = self.cog
if cog is not None:
@ -1335,6 +1354,8 @@ class Group(GroupMixin, Command):
injected = hooked_wrapped_callback(self, ctx, self.callback)
await injected(*ctx.args, **ctx.kwargs)
ctx.invoked_parents.append(ctx.invoked_with)
if trigger and ctx.invoked_subcommand:
ctx.invoked_with = trigger
await ctx.invoked_subcommand.invoke(ctx)
@ -1373,6 +1394,8 @@ class Group(GroupMixin, Command):
if call_hooks:
await self.call_after_hooks(ctx)
ctx.invoked_parents.append(ctx.invoked_with)
if trigger and ctx.invoked_subcommand:
ctx.invoked_with = trigger
await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks)
@ -1578,7 +1601,7 @@ def check_any(*checks):
try:
pred = wrapped.predicate
except AttributeError:
raise TypeError('%r must be wrapped by commands.check decorator' % wrapped) from None
raise TypeError(f'{wrapped!r} must be wrapped by commands.check decorator') from None
else:
unwrapped.append(pred)
@ -1766,7 +1789,7 @@ def has_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx):
ch = ctx.channel
@ -1791,7 +1814,7 @@ def bot_has_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx):
guild = ctx.guild
@ -1819,7 +1842,7 @@ def has_guild_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx):
if not ctx.guild:
@ -1844,7 +1867,7 @@ def bot_has_guild_permissions(**perms):
invalid = set(perms) - set(discord.Permissions.VALID_FLAGS)
if invalid:
raise TypeError('Invalid permission(s): %s' % (', '.join(invalid)))
raise TypeError(f"Invalid permission(s): {', '.join(invalid)}")
def predicate(ctx):
if not ctx.guild:
@ -1949,8 +1972,11 @@ def cooldown(rate, per, type=BucketType.default):
The number of times a command can be used before triggering a cooldown.
per: :class:`float`
The amount of seconds to wait for a cooldown when it's been triggered.
type: :class:`.BucketType`
The type of cooldown to have.
type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]]
The type of cooldown to have. If callable, should return a key for the mapping.
.. versionchanged:: 1.7
Callables are now supported for custom bucket types.
"""
def decorator(func):

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -45,6 +43,7 @@ __all__ = (
'NotOwner',
'MessageNotFound',
'MemberNotFound',
'GuildNotFound',
'UserNotFound',
'ChannelNotFound',
'ChannelNotReadable',
@ -142,7 +141,7 @@ class MissingRequiredArgument(UserInputError):
"""
def __init__(self, param):
self.param = param
super().__init__('{0.name} is a required argument that is missing.'.format(param))
super().__init__(f'{param.name} is a required argument that is missing.')
class TooManyArguments(UserInputError):
"""Exception raised when the command was passed too many arguments and its
@ -228,7 +227,23 @@ class MemberNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Member "{}" not found.'.format(argument))
super().__init__(f'Member "{argument}" not found.')
class GuildNotFound(BadArgument):
"""Exception raised when the guild provided was not found in the bot's cache.
This inherits from :exc:`BadArgument`
.. versionadded:: 1.7
Attributes
-----------
argument: :class:`str`
The guild supplied by the called that was not found
"""
def __init__(self, argument):
self.argument = argument
super().__init__(f'Guild "{argument}" not found.')
class UserNotFound(BadArgument):
"""Exception raised when the user provided was not found in the bot's
@ -245,7 +260,7 @@ class UserNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('User "{}" not found.'.format(argument))
super().__init__(f'User "{argument}" not found.')
class MessageNotFound(BadArgument):
"""Exception raised when the message provided was not found in the channel.
@ -261,7 +276,7 @@ class MessageNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Message "{}" not found.'.format(argument))
super().__init__(f'Message "{argument}" not found.')
class ChannelNotReadable(BadArgument):
"""Exception raised when the bot does not have permission to read messages
@ -278,7 +293,7 @@ class ChannelNotReadable(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__("Can't read messages in {}.".format(argument.mention))
super().__init__(f"Can't read messages in {argument.mention}.")
class ChannelNotFound(BadArgument):
"""Exception raised when the bot can not find the channel.
@ -294,7 +309,7 @@ class ChannelNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Channel "{}" not found.'.format(argument))
super().__init__(f'Channel "{argument}" not found.')
class BadColourArgument(BadArgument):
"""Exception raised when the colour is not valid.
@ -310,7 +325,7 @@ class BadColourArgument(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Colour "{}" is invalid.'.format(argument))
super().__init__(f'Colour "{argument}" is invalid.')
BadColorArgument = BadColourArgument
@ -328,7 +343,7 @@ class RoleNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Role "{}" not found.'.format(argument))
super().__init__(f'Role "{argument}" not found.')
class BadInviteArgument(BadArgument):
"""Exception raised when the invite is invalid or expired.
@ -354,7 +369,7 @@ class EmojiNotFound(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Emoji "{}" not found.'.format(argument))
super().__init__(f'Emoji "{argument}" not found.')
class PartialEmojiConversionFailure(BadArgument):
"""Exception raised when the emoji provided does not match the correct
@ -371,7 +386,7 @@ class PartialEmojiConversionFailure(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('Couldn\'t convert "{}" to PartialEmoji.'.format(argument))
super().__init__(f'Couldn\'t convert "{argument}" to PartialEmoji.')
class BadBoolArgument(BadArgument):
"""Exception raised when a boolean argument was not convertable.
@ -387,7 +402,7 @@ class BadBoolArgument(BadArgument):
"""
def __init__(self, argument):
self.argument = argument
super().__init__('{} is not a recognised boolean option'.format(argument))
super().__init__(f'{argument} is not a recognised boolean option')
class DisabledCommand(CommandError):
"""Exception raised when the command being invoked is disabled.
@ -427,7 +442,7 @@ class CommandOnCooldown(CommandError):
def __init__(self, cooldown, retry_after):
self.cooldown = cooldown
self.retry_after = retry_after
super().__init__('You are on cooldown. Try again in {:.2f}s'.format(retry_after))
super().__init__(f'You are on cooldown. Try again in {retry_after:.2f}s')
class MaxConcurrencyReached(CommandError):
"""Exception raised when the command being invoked has reached its maximum concurrency.
@ -449,7 +464,7 @@ class MaxConcurrencyReached(CommandError):
suffix = 'per %s' % name if per.name != 'default' else 'globally'
plural = '%s times %s' if number > 1 else '%s time %s'
fmt = plural % (number, suffix)
super().__init__('Too many people using this command. It can only be used {} concurrently.'.format(fmt))
super().__init__(f'Too many people using this command. It can only be used {fmt} concurrently.')
class MissingRole(CheckFailure):
"""Exception raised when the command invoker lacks a role to run a command.
@ -466,7 +481,7 @@ class MissingRole(CheckFailure):
"""
def __init__(self, missing_role):
self.missing_role = missing_role
message = 'Role {0!r} is required to run this command.'.format(missing_role)
message = f'Role {missing_role!r} is required to run this command.'
super().__init__(message)
class BotMissingRole(CheckFailure):
@ -484,7 +499,7 @@ class BotMissingRole(CheckFailure):
"""
def __init__(self, missing_role):
self.missing_role = missing_role
message = 'Bot requires the role {0!r} to run this command'.format(missing_role)
message = f'Bot requires the role {missing_role!r} to run this command'
super().__init__(message)
class MissingAnyRole(CheckFailure):
@ -504,14 +519,14 @@ class MissingAnyRole(CheckFailure):
def __init__(self, missing_roles):
self.missing_roles = missing_roles
missing = ["'{}'".format(role) for role in missing_roles]
missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2:
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
message = "You are missing at least one of the required roles: {}".format(fmt)
message = f"You are missing at least one of the required roles: {fmt}"
super().__init__(message)
@ -533,14 +548,14 @@ class BotMissingAnyRole(CheckFailure):
def __init__(self, missing_roles):
self.missing_roles = missing_roles
missing = ["'{}'".format(role) for role in missing_roles]
missing = [f"'{role}'" for role in missing_roles]
if len(missing) > 2:
fmt = '{}, or {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' or '.join(missing)
message = "Bot is missing at least one of the required roles: {}".format(fmt)
message = f"Bot is missing at least one of the required roles: {fmt}"
super().__init__(message)
class NSFWChannelRequired(CheckFailure):
@ -557,7 +572,7 @@ class NSFWChannelRequired(CheckFailure):
"""
def __init__(self, channel):
self.channel = channel
super().__init__("Channel '{}' needs to be NSFW for this command to work.".format(channel))
super().__init__(f"Channel '{channel}' needs to be NSFW for this command to work.")
class MissingPermissions(CheckFailure):
"""Exception raised when the command invoker lacks permissions to run a
@ -579,7 +594,7 @@ class MissingPermissions(CheckFailure):
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = 'You are missing {} permission(s) to run this command.'.format(fmt)
message = f'You are missing {fmt} permission(s) to run this command.'
super().__init__(message, *args)
class BotMissingPermissions(CheckFailure):
@ -602,7 +617,7 @@ class BotMissingPermissions(CheckFailure):
fmt = '{}, and {}'.format(", ".join(missing[:-1]), missing[-1])
else:
fmt = ' and '.join(missing)
message = 'Bot requires {} permission(s) to run this command.'.format(fmt)
message = f'Bot requires {fmt} permission(s) to run this command.'
super().__init__(message, *args)
class BadUnionArgument(UserInputError):
@ -637,7 +652,7 @@ class BadUnionArgument(UserInputError):
else:
fmt = ' or '.join(to_string)
super().__init__('Could not convert "{0.name}" into {1}.'.format(param, fmt))
super().__init__(f'Could not convert "{param.name}" into {fmt}.')
class ArgumentParsingError(UserInputError):
"""An exception raised when the parser fails to parse a user's input.
@ -661,7 +676,7 @@ class UnexpectedQuoteError(ArgumentParsingError):
"""
def __init__(self, quote):
self.quote = quote
super().__init__('Unexpected quote mark, {0!r}, in non-quoted string'.format(quote))
super().__init__(f'Unexpected quote mark, {quote!r}, in non-quoted string')
class InvalidEndOfQuotedStringError(ArgumentParsingError):
"""An exception raised when a space is expected after the closing quote in a string
@ -676,7 +691,7 @@ class InvalidEndOfQuotedStringError(ArgumentParsingError):
"""
def __init__(self, char):
self.char = char
super().__init__('Expected space after closing quotation but received {0!r}'.format(char))
super().__init__(f'Expected space after closing quotation but received {char!r}')
class ExpectedClosingQuoteError(ArgumentParsingError):
"""An exception raised when a quote character is expected but not found.
@ -691,7 +706,7 @@ class ExpectedClosingQuoteError(ArgumentParsingError):
def __init__(self, close_quote):
self.close_quote = close_quote
super().__init__('Expected closing {}.'.format(close_quote))
super().__init__(f'Expected closing {close_quote}.')
class ExtensionError(DiscordException):
"""Base exception for extension related errors.
@ -705,7 +720,7 @@ class ExtensionError(DiscordException):
"""
def __init__(self, message=None, *args, name):
self.name = name
message = message or 'Extension {!r} had an error.'.format(name)
message = message or f'Extension {name!r} had an error.'
# clean-up @everyone and @here mentions
m = message.replace('@everyone', '@\u200beveryone').replace('@here', '@\u200bhere')
super().__init__(m, *args)
@ -716,7 +731,7 @@ class ExtensionAlreadyLoaded(ExtensionError):
This inherits from :exc:`ExtensionError`
"""
def __init__(self, name):
super().__init__('Extension {!r} is already loaded.'.format(name), name=name)
super().__init__(f'Extension {name!r} is already loaded.', name=name)
class ExtensionNotLoaded(ExtensionError):
"""An exception raised when an extension was not loaded.
@ -724,7 +739,7 @@ class ExtensionNotLoaded(ExtensionError):
This inherits from :exc:`ExtensionError`
"""
def __init__(self, name):
super().__init__('Extension {!r} has not been loaded.'.format(name), name=name)
super().__init__(f'Extension {name!r} has not been loaded.', name=name)
class NoEntryPointError(ExtensionError):
"""An exception raised when an extension does not have a ``setup`` entry point function.
@ -732,7 +747,7 @@ class NoEntryPointError(ExtensionError):
This inherits from :exc:`ExtensionError`
"""
def __init__(self, name):
super().__init__("Extension {!r} has no 'setup' function.".format(name), name=name)
super().__init__(f"Extension {name!r} has no 'setup' function.", name=name)
class ExtensionFailed(ExtensionError):
"""An exception raised when an extension failed to load during execution of the module or ``setup`` entry point.
@ -791,4 +806,4 @@ class CommandRegistrationError(ClientException):
self.name = name
self.alias_conflict = alias_conflict
type_ = 'alias' if alias_conflict else 'command'
super().__init__('The {} {} is already an existing command or alias.'.format(type_, name))
super().__init__(f'The {type_} {name} is already an existing command or alias.')

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
@ -79,18 +77,22 @@ class Paginator:
The suffix appended at the end of every page. e.g. three backticks.
max_size: :class:`int`
The maximum amount of codepoints allowed in a page.
linesep: :class:`str`
The character string inserted between lines. e.g. a newline character.
.. versionadded:: 1.7
"""
def __init__(self, prefix='```', suffix='```', max_size=2000):
def __init__(self, prefix='```', suffix='```', max_size=2000, linesep='\n'):
self.prefix = prefix
self.suffix = suffix
self.max_size = max_size
self.linesep = linesep
self.clear()
def clear(self):
"""Clears the paginator to have no pages."""
if self.prefix is not None:
self._current_page = [self.prefix]
self._count = len(self.prefix) + 1 # prefix + newline
self._count = len(self.prefix) + self._linesep_len # prefix + newline
else:
self._current_page = []
self._count = 0
@ -104,6 +106,10 @@ class Paginator:
def _suffix_len(self):
return len(self.suffix) if self.suffix else 0
@property
def _linesep_len(self):
return len(self.linesep)
def add_line(self, line='', *, empty=False):
"""Adds a line to the current page.
@ -122,29 +128,29 @@ class Paginator:
RuntimeError
The line was too big for the current :attr:`max_size`.
"""
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2
max_page_size = self.max_size - self._prefix_len - self._suffix_len - 2 * self._linesep_len
if len(line) > max_page_size:
raise RuntimeError('Line exceeds maximum page size %s' % (max_page_size))
raise RuntimeError(f'Line exceeds maximum page size {max_page_size}')
if self._count + len(line) + 1 > self.max_size - self._suffix_len:
if self._count + len(line) + self._linesep_len > self.max_size - self._suffix_len:
self.close_page()
self._count += len(line) + 1
self._count += len(line) + self._linesep_len
self._current_page.append(line)
if empty:
self._current_page.append('')
self._count += 1
self._count += self._linesep_len
def close_page(self):
"""Prematurely terminate a page."""
if self.suffix is not None:
self._current_page.append(self.suffix)
self._pages.append('\n'.join(self._current_page))
self._pages.append(self.linesep.join(self._current_page))
if self.prefix is not None:
self._current_page = [self.prefix]
self._count = len(self.prefix) + 1 # prefix + newline
self._count = len(self.prefix) + self._linesep_len # prefix + linesep
else:
self._current_page = []
self._count = 0
@ -162,7 +168,7 @@ class Paginator:
return self._pages
def __repr__(self):
fmt = '<Paginator prefix: {0.prefix} suffix: {0.suffix} max_size: {0.max_size} count: {0._count}>'
fmt = '<Paginator prefix: {0.prefix!r} suffix: {0.suffix!r} linesep: {0.linesep!r} max_size: {0.max_size} count: {0._count}>'
return fmt.format(self)
def _not_overriden(f):
@ -264,9 +270,13 @@ class HelpCommand:
show_hidden: :class:`bool`
Specifies if hidden commands should be shown in the output.
Defaults to ``False``.
verify_checks: :class:`bool`
verify_checks: Optional[:class:`bool`]
Specifies if commands should have their :attr:`.Command.checks` called
and verified. Defaults to ``True``.
and verified. If ``True``, always calls :attr:`.Commands.checks`.
If ``None``, only calls :attr:`.Commands.checks` in a guild setting.
If ``False``, never calls :attr:`.Commands.checks`. Defaults to ``True``.
..versionchanged:: 1.7
command_attrs: :class:`dict`
A dictionary of options to pass in for the construction of the help command.
This allows you to change the command behaviour without actually changing
@ -309,7 +319,7 @@ class HelpCommand:
attrs.setdefault('name', 'help')
attrs.setdefault('help', 'Shows this message')
self.context = None
self._command_impl = None
self._command_impl = _HelpCommandImpl(self, **self.command_attrs)
def copy(self):
obj = self.__class__(*self.__original_args__, **self.__original_kwargs__)
@ -324,7 +334,6 @@ class HelpCommand:
def _remove_from_bot(self, bot):
bot.remove_command(self._command_impl.name)
self._command_impl._eject_cog()
self._command_impl = None
def add_check(self, func):
"""
@ -338,13 +347,7 @@ class HelpCommand:
The function that will be used as a check.
"""
if self._command_impl is not None:
self._command_impl.add_check(func)
else:
try:
self.command_attrs["checks"].append(func)
except KeyError:
self.command_attrs["checks"] = [func]
self._command_impl.add_check(func)
def remove_check(self, func):
"""
@ -361,13 +364,7 @@ class HelpCommand:
The function to remove from the checks.
"""
if self._command_impl is not None:
self._command_impl.remove_check(func)
else:
try:
self.command_attrs["checks"].remove(func)
except (KeyError, ValueError):
pass
self._command_impl.remove_check(func)
def get_bot_mapping(self):
"""Retrieves the bot mapping passed to :meth:`send_bot_help`."""
@ -376,7 +373,7 @@ class HelpCommand:
cog: cog.get_commands()
for cog in bot.cogs.values()
}
mapping[None] = [c for c in bot.all_commands.values() if c.cog is None]
mapping[None] = [c for c in bot.commands if c.cog is None]
return mapping
@property
@ -387,8 +384,9 @@ class HelpCommand:
# consider this to be an *incredibly* strange use case. I'd rather go
# for this common use case rather than waste performance for the
# odd one.
pattern = re.compile(r"<@!?%s>" % user.id)
return pattern.sub("@%s" % user.display_name.replace('\\', r'\\'), self.context.prefix)
pattern = re.compile(fr"<@!?{user.id}>")
display_name = user.display_name.replace('\\', r'\\')
return pattern.sub('@' + display_name, self.context.prefix)
@property
def invoked_with(self):
@ -425,17 +423,26 @@ class HelpCommand:
The signature for the command.
"""
parent = command.full_parent_name
parent = command.parent
entries = []
while parent is not None:
if not parent.signature or parent.invoke_without_command:
entries.append(parent.name)
else:
entries.append(parent.name + ' ' + parent.signature)
parent = parent.parent
parent_sig = ' '.join(reversed(entries))
if len(command.aliases) > 0:
aliases = '|'.join(command.aliases)
fmt = '[%s|%s]' % (command.name, aliases)
if parent:
fmt = parent + ' ' + fmt
fmt = f'[{command.name}|{aliases}]'
if parent_sig:
fmt = parent_sig + ' ' + fmt
alias = fmt
else:
alias = command.name if not parent else parent + ' ' + command.name
alias = command.name if not parent_sig else parent_sig + ' ' + command.name
return '%s%s %s' % (self.clean_prefix, alias, command.signature)
return f'{self.clean_prefix}{alias} {command.signature}'
def remove_mentions(self, string):
"""Removes mentions from the string to prevent abuse.
@ -498,7 +505,7 @@ class HelpCommand:
:class:`str`
The string to use when a command has not been found.
"""
return 'No command called "{}" found.'.format(string)
return f'No command called "{string}" found.'
def subcommand_not_found(self, command, string):
"""|maybecoro|
@ -527,8 +534,8 @@ class HelpCommand:
The string to use when the command did not have the subcommand requested.
"""
if isinstance(command, Group) and len(command.all_commands) > 0:
return 'Command "{0.qualified_name}" has no subcommand named {1}'.format(command, string)
return 'Command "{0.qualified_name}" has no subcommands.'.format(command)
return f'Command "{command.qualified_name}" has no subcommand named {string}'
return f'Command "{command.qualified_name}" has no subcommands.'
async def filter_commands(self, commands, *, sort=False, key=None):
"""|coro|
@ -560,11 +567,15 @@ class HelpCommand:
iterator = commands if self.show_hidden else filter(lambda c: not c.hidden, commands)
if not self.verify_checks:
if self.verify_checks is False:
# if we do not need to verify the checks then we can just
# run it straight through normally without using await.
return sorted(iterator, key=key) if sort else list(iterator)
if self.verify_checks is None and not self.context.guild:
# if verify_checks is None and we're in a DM, don't verify
return sorted(iterator, key=key) if sort else list(iterator)
# if we're here then we need to check every command if it can run
async def predicate(cmd):
try:
@ -929,8 +940,8 @@ class DefaultHelpCommand(HelpCommand):
def get_ending_note(self):
""":class:`str`: Returns help command's ending note. This is mainly useful to override for i18n purposes."""
command_name = self.invoked_with
return "Type {0}{1} command for more info on a command.\n" \
"You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name)
return f"Type {self.clean_prefix}{command_name} command for more info on a command.\n" \
f"You can also type {self.clean_prefix}{command_name} category for more info on a category."
def add_indented_commands(self, commands, *, heading, max_size=None):
"""Indents a list of commands after the specified heading.
@ -965,7 +976,7 @@ class DefaultHelpCommand(HelpCommand):
for command in commands:
name = command.name
width = max_size - (get_width(name) - len(name))
entry = '{0}{1:<{width}} {2}'.format(self.indent * ' ', name, command.short_doc, width=width)
entry = f'{self.indent * " "}{name:<{width}} {command.short_doc}'
self.paginator.add_line(self.shorten_text(entry))
async def send_pages(self):
@ -1018,7 +1029,7 @@ class DefaultHelpCommand(HelpCommand):
# <description> portion
self.paginator.add_line(bot.description, empty=True)
no_category = '\u200b{0.no_category}:'.format(self)
no_category = f'\u200b{self.no_category}:'
def get_category(command, *, no_category=no_category):
cog = command.cog
return cog.qualified_name + ':' if cog is not None else no_category
@ -1142,7 +1153,7 @@ class MinimalHelpCommand(HelpCommand):
"You can also use `{0}{1} [category]` for more info on a category.".format(self.clean_prefix, command_name)
def get_command_signature(self, command):
return '%s%s %s' % (self.clean_prefix, command.qualified_name, command.signature)
return f'{self.clean_prefix}{command.qualified_name} {command.signature}'
def get_ending_note(self):
"""Return the help command's ending note. This is mainly useful to override for i18n purposes.
@ -1174,7 +1185,7 @@ class MinimalHelpCommand(HelpCommand):
if commands:
# U+2002 Middle Dot
joined = '\u2002'.join(c.name for c in commands)
self.paginator.add_line('__**%s**__' % heading)
self.paginator.add_line(f'__**{heading}**__')
self.paginator.add_line(joined)
def add_subcommand_formatting(self, command):
@ -1208,7 +1219,7 @@ class MinimalHelpCommand(HelpCommand):
aliases: Sequence[:class:`str`]
A list of aliases to format.
"""
self.paginator.add_line('**%s** %s' % (self.aliases_heading, ', '.join(aliases)), empty=True)
self.paginator.add_line(f'**{self.aliases_heading}** {", ".join(aliases)}', empty=True)
def add_command_formatting(self, command):
"""A utility function to format commands and groups.
@ -1261,7 +1272,7 @@ class MinimalHelpCommand(HelpCommand):
if note:
self.paginator.add_line(note, empty=True)
no_category = '\u200b{0.no_category}'.format(self)
no_category = f'\u200b{self.no_category}'
def get_category(command, *, no_category=no_category):
cog = command.cog
return cog.qualified_name if cog is not None else no_category
@ -1294,7 +1305,7 @@ class MinimalHelpCommand(HelpCommand):
filtered = await self.filter_commands(cog.get_commands(), sort=self.sort_commands)
if filtered:
self.paginator.add_line('**%s %s**' % (cog.qualified_name, self.commands_heading))
self.paginator.add_line(f'**{cog.qualified_name} {self.commands_heading}**')
for command in filtered:
self.add_subcommand_formatting(command)
@ -1314,7 +1325,7 @@ class MinimalHelpCommand(HelpCommand):
if note:
self.paginator.add_line(note, empty=True)
self.paginator.add_line('**%s**' % self.commands_heading)
self.paginator.add_line(f'**{self.commands_heading}**')
for command in filtered:
self.add_subcommand_formatting(command)

View File

@ -1,9 +1,7 @@
# -*- coding: utf-8 -*-
"""
The MIT License (MIT)
Copyright (c) 2015-2020 Rapptz
Copyright (c) 2015-present Rapptz
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),