mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-07-02 00:00:02 +00:00
[commands] Add run_converters helper to call converters
This commit is contained in:
parent
09f3f2111c
commit
c54e43360b
@ -26,7 +26,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Iterable, Optional, TYPE_CHECKING, List, Protocol, Type, TypeVar, Tuple, Union, runtime_checkable
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
List,
|
||||||
|
Protocol,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
runtime_checkable,
|
||||||
|
)
|
||||||
|
|
||||||
import discord
|
import discord
|
||||||
from .errors import *
|
from .errors import *
|
||||||
@ -58,6 +72,7 @@ __all__ = (
|
|||||||
'StoreChannelConverter',
|
'StoreChannelConverter',
|
||||||
'clean_content',
|
'clean_content',
|
||||||
'Greedy',
|
'Greedy',
|
||||||
|
'run_converters',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -867,3 +882,163 @@ class Greedy(List[T]):
|
|||||||
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
raise TypeError(f'Greedy[{converter!r}] is invalid.')
|
||||||
|
|
||||||
return cls(converter=converter)
|
return cls(converter=converter)
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_to_bool(argument: str) -> bool:
|
||||||
|
lowered = argument.lower()
|
||||||
|
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
||||||
|
return True
|
||||||
|
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise BadBoolArgument(lowered)
|
||||||
|
|
||||||
|
|
||||||
|
def get_converter(param: inspect.Parameter) -> Any:
|
||||||
|
converter = param.annotation
|
||||||
|
if converter is param.empty:
|
||||||
|
if param.default is not param.empty:
|
||||||
|
converter = str if param.default is None else type(param.default)
|
||||||
|
else:
|
||||||
|
converter = str
|
||||||
|
return converter
|
||||||
|
|
||||||
|
|
||||||
|
CONVERTER_MAPPING: Dict[Type[Any], Any] = {
|
||||||
|
discord.Object: ObjectConverter,
|
||||||
|
discord.Member: MemberConverter,
|
||||||
|
discord.User: UserConverter,
|
||||||
|
discord.Message: MessageConverter,
|
||||||
|
discord.PartialMessage: PartialMessageConverter,
|
||||||
|
discord.TextChannel: TextChannelConverter,
|
||||||
|
discord.Invite: InviteConverter,
|
||||||
|
discord.Guild: GuildConverter,
|
||||||
|
discord.Role: RoleConverter,
|
||||||
|
discord.Game: GameConverter,
|
||||||
|
discord.Colour: ColourConverter,
|
||||||
|
discord.VoiceChannel: VoiceChannelConverter,
|
||||||
|
discord.StageChannel: StageChannelConverter,
|
||||||
|
discord.Emoji: EmojiConverter,
|
||||||
|
discord.PartialEmoji: PartialEmojiConverter,
|
||||||
|
discord.CategoryChannel: CategoryChannelConverter,
|
||||||
|
discord.StoreChannel: StoreChannelConverter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _actual_conversion(ctx: Context, converter, argument: str, param: inspect.Parameter):
|
||||||
|
if converter is bool:
|
||||||
|
return _convert_to_bool(argument)
|
||||||
|
|
||||||
|
try:
|
||||||
|
module = converter.__module__
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
|
||||||
|
converter = CONVERTER_MAPPING.get(converter, converter)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if inspect.isclass(converter) and issubclass(converter, Converter):
|
||||||
|
if inspect.ismethod(converter.convert):
|
||||||
|
return await converter.convert(ctx, argument)
|
||||||
|
else:
|
||||||
|
return await converter().convert(ctx, argument)
|
||||||
|
elif isinstance(converter, Converter):
|
||||||
|
return await converter.convert(ctx, argument)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise ConversionError(converter, exc) from exc
|
||||||
|
|
||||||
|
try:
|
||||||
|
return converter(argument)
|
||||||
|
except CommandError:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
try:
|
||||||
|
name = converter.__name__
|
||||||
|
except AttributeError:
|
||||||
|
name = converter.__class__.__name__
|
||||||
|
|
||||||
|
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
||||||
|
|
||||||
|
|
||||||
|
async def run_converters(ctx: Context, converter, argument: str, param: inspect.Parameter):
|
||||||
|
"""|coro|
|
||||||
|
|
||||||
|
Runs converters for a given converter, argument, and parameter.
|
||||||
|
|
||||||
|
This function does the same work that the library does under the hood.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
------------
|
||||||
|
ctx: :class:`Context`
|
||||||
|
The invocation context to run the converters under.
|
||||||
|
converter: Any
|
||||||
|
The converter to run, this corresponds to the annotation in the function.
|
||||||
|
argument: :class:`str`
|
||||||
|
The argument to convert to.
|
||||||
|
param: :class:`inspect.Parameter`
|
||||||
|
The parameter being converted. This is mainly for error reporting.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
-------
|
||||||
|
CommandError
|
||||||
|
The converter failed to convert.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
--------
|
||||||
|
Any
|
||||||
|
The resulting conversion.
|
||||||
|
"""
|
||||||
|
origin = getattr(converter, '__origin__', None)
|
||||||
|
|
||||||
|
if origin is Union:
|
||||||
|
errors = []
|
||||||
|
_NoneType = type(None)
|
||||||
|
union_args = converter.__args__
|
||||||
|
for conv in union_args:
|
||||||
|
# if we got to this part in the code, then the previous conversions have failed
|
||||||
|
# so we should just undo the view, return the default, and allow parsing to continue
|
||||||
|
# with the other parameters
|
||||||
|
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
|
||||||
|
ctx.view.undo()
|
||||||
|
return None if param.default is param.empty else param.default
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = await run_converters(ctx, conv, argument, param)
|
||||||
|
except CommandError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# if we're here, then we failed all the converters
|
||||||
|
raise BadUnionArgument(param, union_args, errors)
|
||||||
|
|
||||||
|
if origin is Literal:
|
||||||
|
errors = []
|
||||||
|
conversions = {}
|
||||||
|
literal_args = converter.__args__
|
||||||
|
for literal in literal_args:
|
||||||
|
literal_type = type(literal)
|
||||||
|
try:
|
||||||
|
value = conversions[literal_type]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
value = await _actual_conversion(ctx, literal_type, argument, param)
|
||||||
|
except CommandError as exc:
|
||||||
|
errors.append(exc)
|
||||||
|
conversions[literal_type] = object()
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
conversions[literal_type] = value
|
||||||
|
|
||||||
|
if value == literal:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# if we're here, then we failed to match all the literals
|
||||||
|
raise BadLiteralArgument(param, literal_args, errors)
|
||||||
|
|
||||||
|
return await _actual_conversion(ctx, converter, argument, param)
|
||||||
|
@ -43,7 +43,7 @@ import discord
|
|||||||
|
|
||||||
from .errors import *
|
from .errors import *
|
||||||
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
|
from .cooldowns import Cooldown, BucketType, CooldownMapping, MaxConcurrency, DynamicCooldownMapping
|
||||||
from . import converter as converters
|
from .converter import run_converters, get_converter, Greedy
|
||||||
from ._types import _BaseCommand
|
from ._types import _BaseCommand
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
|
|
||||||
@ -175,7 +175,7 @@ def get_signature_parameters(function: types.FunctionType) -> Dict[str, inspect.
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
annotation = _evaluate_annotation(annotation, globalns, globalns, cache)
|
annotation = _evaluate_annotation(annotation, globalns, globalns, cache)
|
||||||
if annotation is converters.Greedy:
|
if annotation is Greedy:
|
||||||
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
|
raise TypeError('Unparameterized Greedy[...] is disallowed in signature.')
|
||||||
|
|
||||||
params[name] = parameter.replace(annotation=annotation)
|
params[name] = parameter.replace(annotation=annotation)
|
||||||
@ -219,14 +219,6 @@ def hooked_wrapped_callback(command, ctx, coro):
|
|||||||
return ret
|
return ret
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
def _convert_to_bool(argument):
|
|
||||||
lowered = argument.lower()
|
|
||||||
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
|
||||||
return True
|
|
||||||
elif lowered in ('no', 'n', 'false', 'f', '0', 'disable', 'off'):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
raise BadBoolArgument(lowered)
|
|
||||||
|
|
||||||
class _CaseInsensitiveDict(dict):
|
class _CaseInsensitiveDict(dict):
|
||||||
def __contains__(self, k):
|
def __contains__(self, k):
|
||||||
@ -541,113 +533,16 @@ class Command(_BaseCommand):
|
|||||||
finally:
|
finally:
|
||||||
ctx.bot.dispatch('command_error', ctx, error)
|
ctx.bot.dispatch('command_error', ctx, error)
|
||||||
|
|
||||||
async def _actual_conversion(self, ctx, converter, argument, param):
|
|
||||||
if converter is bool:
|
|
||||||
return _convert_to_bool(argument)
|
|
||||||
|
|
||||||
try:
|
|
||||||
module = converter.__module__
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if module is not None and (module.startswith('discord.') and not module.endswith('converter')):
|
|
||||||
converter = getattr(converters, converter.__name__ + 'Converter', converter)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if inspect.isclass(converter) and issubclass(converter, converters.Converter):
|
|
||||||
if inspect.ismethod(converter.convert):
|
|
||||||
return await converter.convert(ctx, argument)
|
|
||||||
else:
|
|
||||||
return await converter().convert(ctx, argument)
|
|
||||||
elif isinstance(converter, converters.Converter):
|
|
||||||
return await converter.convert(ctx, argument)
|
|
||||||
except CommandError:
|
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
raise ConversionError(converter, exc) from exc
|
|
||||||
|
|
||||||
try:
|
|
||||||
return converter(argument)
|
|
||||||
except CommandError:
|
|
||||||
raise
|
|
||||||
except Exception as exc:
|
|
||||||
try:
|
|
||||||
name = converter.__name__
|
|
||||||
except AttributeError:
|
|
||||||
name = converter.__class__.__name__
|
|
||||||
|
|
||||||
raise BadArgument(f'Converting to "{name}" failed for parameter "{param.name}".') from exc
|
|
||||||
|
|
||||||
async def do_conversion(self, ctx, converter, argument, param):
|
|
||||||
origin = getattr(converter, '__origin__', None)
|
|
||||||
|
|
||||||
if origin is Union:
|
|
||||||
errors = []
|
|
||||||
_NoneType = type(None)
|
|
||||||
union_args = converter.__args__
|
|
||||||
for conv in union_args:
|
|
||||||
# if we got to this part in the code, then the previous conversions have failed
|
|
||||||
# so we should just undo the view, return the default, and allow parsing to continue
|
|
||||||
# with the other parameters
|
|
||||||
if conv is _NoneType and param.kind != param.VAR_POSITIONAL:
|
|
||||||
ctx.view.undo()
|
|
||||||
return None if param.default is param.empty else param.default
|
|
||||||
|
|
||||||
try:
|
|
||||||
value = await self.do_conversion(ctx, conv, argument, param)
|
|
||||||
except CommandError as exc:
|
|
||||||
errors.append(exc)
|
|
||||||
else:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# if we're here, then we failed all the converters
|
|
||||||
raise BadUnionArgument(param, union_args, errors)
|
|
||||||
|
|
||||||
if origin is Literal:
|
|
||||||
errors = []
|
|
||||||
conversions = {}
|
|
||||||
literal_args = converter.__args__
|
|
||||||
for literal in literal_args:
|
|
||||||
literal_type = type(literal)
|
|
||||||
try:
|
|
||||||
value = conversions[literal_type]
|
|
||||||
except KeyError:
|
|
||||||
try:
|
|
||||||
value = await self._actual_conversion(ctx, literal_type, argument, param)
|
|
||||||
except CommandError as exc:
|
|
||||||
errors.append(exc)
|
|
||||||
conversions[literal_type] = object()
|
|
||||||
continue
|
|
||||||
else:
|
|
||||||
conversions[literal_type] = value
|
|
||||||
|
|
||||||
if value == literal:
|
|
||||||
return value
|
|
||||||
|
|
||||||
# if we're here, then we failed to match all the literals
|
|
||||||
raise BadLiteralArgument(param, literal_args, errors)
|
|
||||||
|
|
||||||
return await self._actual_conversion(ctx, converter, argument, param)
|
|
||||||
|
|
||||||
def _get_converter(self, param):
|
|
||||||
converter = param.annotation
|
|
||||||
if converter is param.empty:
|
|
||||||
if param.default is not param.empty:
|
|
||||||
converter = str if param.default is None else type(param.default)
|
|
||||||
else:
|
|
||||||
converter = str
|
|
||||||
return converter
|
|
||||||
|
|
||||||
async def transform(self, ctx, param):
|
async def transform(self, ctx, param):
|
||||||
required = param.default is param.empty
|
required = param.default is param.empty
|
||||||
converter = self._get_converter(param)
|
converter = get_converter(param)
|
||||||
consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
|
consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
|
|
||||||
# The greedy converter is simple -- it keeps going until it fails in which case,
|
# 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
|
# it undos the view ready for the next parameter to use instead
|
||||||
if isinstance(converter, converters.Greedy):
|
if isinstance(converter, Greedy):
|
||||||
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
|
if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY):
|
||||||
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
return await self._transform_greedy_pos(ctx, param, required, converter.converter)
|
||||||
elif param.kind == param.VAR_POSITIONAL:
|
elif param.kind == param.VAR_POSITIONAL:
|
||||||
@ -674,7 +569,7 @@ class Command(_BaseCommand):
|
|||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
view.previous = previous
|
view.previous = previous
|
||||||
|
|
||||||
return await self.do_conversion(ctx, converter, argument, param)
|
return await run_converters(ctx, converter, argument, param)
|
||||||
|
|
||||||
async def _transform_greedy_pos(self, ctx, param, required, converter):
|
async def _transform_greedy_pos(self, ctx, param, required, converter):
|
||||||
view = ctx.view
|
view = ctx.view
|
||||||
@ -686,7 +581,7 @@ class Command(_BaseCommand):
|
|||||||
view.skip_ws()
|
view.skip_ws()
|
||||||
try:
|
try:
|
||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
value = await self.do_conversion(ctx, converter, argument, param)
|
value = await run_converters(ctx, converter, argument, param)
|
||||||
except (CommandError, ArgumentParsingError):
|
except (CommandError, ArgumentParsingError):
|
||||||
view.index = previous
|
view.index = previous
|
||||||
break
|
break
|
||||||
@ -702,7 +597,7 @@ class Command(_BaseCommand):
|
|||||||
previous = view.index
|
previous = view.index
|
||||||
try:
|
try:
|
||||||
argument = view.get_quoted_word()
|
argument = view.get_quoted_word()
|
||||||
value = await self.do_conversion(ctx, converter, argument, param)
|
value = await run_converters(ctx, converter, argument, param)
|
||||||
except (CommandError, ArgumentParsingError):
|
except (CommandError, ArgumentParsingError):
|
||||||
view.index = previous
|
view.index = previous
|
||||||
raise RuntimeError() from None # break loop
|
raise RuntimeError() from None # break loop
|
||||||
@ -826,9 +721,9 @@ class Command(_BaseCommand):
|
|||||||
elif param.kind == param.KEYWORD_ONLY:
|
elif param.kind == param.KEYWORD_ONLY:
|
||||||
# kwarg only param denotes "consume rest" semantics
|
# kwarg only param denotes "consume rest" semantics
|
||||||
if self.rest_is_raw:
|
if self.rest_is_raw:
|
||||||
converter = self._get_converter(param)
|
converter = get_converter(param)
|
||||||
argument = view.read_rest()
|
argument = view.read_rest()
|
||||||
kwargs[name] = await self.do_conversion(ctx, converter, argument, param)
|
kwargs[name] = await run_converters(ctx, converter, argument, param)
|
||||||
else:
|
else:
|
||||||
kwargs[name] = await self.transform(ctx, param)
|
kwargs[name] = await self.transform(ctx, param)
|
||||||
break
|
break
|
||||||
@ -1126,7 +1021,7 @@ class Command(_BaseCommand):
|
|||||||
|
|
||||||
result = []
|
result = []
|
||||||
for name, param in params.items():
|
for name, param in params.items():
|
||||||
greedy = isinstance(param.annotation, converters.Greedy)
|
greedy = isinstance(param.annotation, Greedy)
|
||||||
optional = False # postpone evaluation of if it's an optional argument
|
optional = False # postpone evaluation of if it's an optional argument
|
||||||
|
|
||||||
# for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the
|
# for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the
|
||||||
|
@ -329,6 +329,8 @@ Converters
|
|||||||
|
|
||||||
.. autoclass:: discord.ext.commands.Greedy()
|
.. autoclass:: discord.ext.commands.Greedy()
|
||||||
|
|
||||||
|
.. autofunction:: discord.ext.commands.run_converters
|
||||||
|
|
||||||
.. _ext_commands_api_errors:
|
.. _ext_commands_api_errors:
|
||||||
|
|
||||||
Exceptions
|
Exceptions
|
||||||
|
Loading…
x
Reference in New Issue
Block a user