mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-06-07 12:18:59 +00:00
[commands] Add Range converter
This allows hybrid commands to also have a range annotation
This commit is contained in:
parent
3c4b22b623
commit
896932faf1
@ -81,6 +81,7 @@ __all__ = (
|
|||||||
'ScheduledEventConverter',
|
'ScheduledEventConverter',
|
||||||
'clean_content',
|
'clean_content',
|
||||||
'Greedy',
|
'Greedy',
|
||||||
|
'Range',
|
||||||
'run_converters',
|
'run_converters',
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1053,6 +1054,84 @@ class Greedy(List[T]):
|
|||||||
return cls(converter=converter)
|
return cls(converter=converter)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing_extensions import Annotated as Range
|
||||||
|
else:
|
||||||
|
|
||||||
|
class Range:
|
||||||
|
"""A special converter that can be applied to a parameter to require a numeric type
|
||||||
|
to fit within the range provided.
|
||||||
|
|
||||||
|
During type checking time this is equivalent to :obj:`typing.Annotated` so type checkers understand
|
||||||
|
the intent of the code.
|
||||||
|
|
||||||
|
Some example ranges:
|
||||||
|
|
||||||
|
- ``Range[int, 10]`` means the minimum is 10 with no maximum.
|
||||||
|
- ``Range[int, None, 10]`` means the maximum is 10 with no minimum.
|
||||||
|
- ``Range[int, 1, 10]`` means the minimum is 1 and the maximum is 10.
|
||||||
|
|
||||||
|
Inside a :class:`HybridCommand` this functions equivalently to :class:`discord.app_commands.Range`.
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Examples
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
@bot.command()
|
||||||
|
async def range(ctx: commands.Context, value: commands.Range[int, 10, 12]):
|
||||||
|
await ctx.send(f'Your value is {value}')
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
annotation: Any,
|
||||||
|
min: Optional[Union[int, float]] = None,
|
||||||
|
max: Optional[Union[int, float]] = None,
|
||||||
|
) -> None:
|
||||||
|
self.annotation: Any = annotation
|
||||||
|
self.min: Optional[Union[int, float]] = min
|
||||||
|
self.max: Optional[Union[int, float]] = max
|
||||||
|
|
||||||
|
async def convert(self, ctx: Context[BotT], value: str) -> Union[int, float]:
|
||||||
|
converted = self.annotation(value)
|
||||||
|
if (self.min is not None and converted < self.min) or (self.max is not None and converted > self.max):
|
||||||
|
raise RangeError(converted, minimum=self.min, maximum=self.max)
|
||||||
|
|
||||||
|
return converted
|
||||||
|
|
||||||
|
def __class_getitem__(cls, obj) -> Range:
|
||||||
|
if not isinstance(obj, tuple):
|
||||||
|
raise TypeError(f'expected tuple for arguments, received {obj.__class__!r} instead')
|
||||||
|
|
||||||
|
if len(obj) == 2:
|
||||||
|
obj = (*obj, None)
|
||||||
|
elif len(obj) != 3:
|
||||||
|
raise TypeError('Range accepts either two or three arguments with the first being the type of range.')
|
||||||
|
|
||||||
|
annotation, min, max = obj
|
||||||
|
|
||||||
|
if min is None and max is None:
|
||||||
|
raise TypeError('Range must not be empty')
|
||||||
|
|
||||||
|
if min is not None and max is not None:
|
||||||
|
# At this point max and min are both not none
|
||||||
|
if type(min) != type(max):
|
||||||
|
raise TypeError('Both min and max in Range must be the same type')
|
||||||
|
|
||||||
|
if annotation not in (int, float):
|
||||||
|
raise TypeError(f'expected int or float as range type, received {annotation!r} instead')
|
||||||
|
|
||||||
|
return cls(
|
||||||
|
annotation=annotation,
|
||||||
|
min=annotation(min) if min is not None else None,
|
||||||
|
max=annotation(max) if max is not None else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _convert_to_bool(argument: str) -> bool:
|
def _convert_to_bool(argument: str) -> bool:
|
||||||
lowered = argument.lower()
|
lowered = argument.lower()
|
||||||
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):
|
||||||
|
@ -102,6 +102,7 @@ __all__ = (
|
|||||||
'TooManyFlags',
|
'TooManyFlags',
|
||||||
'MissingRequiredFlag',
|
'MissingRequiredFlag',
|
||||||
'HybridCommandError',
|
'HybridCommandError',
|
||||||
|
'RangeError',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -555,6 +556,44 @@ class BadBoolArgument(BadArgument):
|
|||||||
super().__init__(f'{argument} is not a recognised boolean option')
|
super().__init__(f'{argument} is not a recognised boolean option')
|
||||||
|
|
||||||
|
|
||||||
|
class RangeError(BadArgument):
|
||||||
|
"""Exception raised when an argument is out of range.
|
||||||
|
|
||||||
|
This inherits from :exc:`BadArgument`
|
||||||
|
|
||||||
|
.. versionadded:: 2.0
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
-----------
|
||||||
|
minimum: Optional[Union[:class:`int`, :class:`float`]]
|
||||||
|
The minimum value expected or ``None`` if there wasn't one
|
||||||
|
maximum: Optional[Union[:class:`int`, :class:`float`]]
|
||||||
|
The maximum value expected or ``None`` if there wasn't one
|
||||||
|
value: Union[:class:`int`, :class:`float`]
|
||||||
|
The value that was out of range.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: Union[int, float],
|
||||||
|
minimum: Optional[Union[int, float]],
|
||||||
|
maximum: Optional[Union[int, float]],
|
||||||
|
) -> None:
|
||||||
|
self.value: Union[int, float] = value
|
||||||
|
self.minimum: Optional[Union[int, float]] = minimum
|
||||||
|
self.maximum: Optional[Union[int, float]] = maximum
|
||||||
|
|
||||||
|
label: str = ''
|
||||||
|
if minimum is None and maximum is not None:
|
||||||
|
label = f'no more than {maximum}'
|
||||||
|
elif minimum is not None and maximum is None:
|
||||||
|
label = f'not less than {minimum}'
|
||||||
|
elif maximum is not None and minimum is not None:
|
||||||
|
label = f'between {minimum} and {maximum}'
|
||||||
|
|
||||||
|
super().__init__(f'value must be {label} but received {value}')
|
||||||
|
|
||||||
|
|
||||||
class DisabledCommand(CommandError):
|
class DisabledCommand(CommandError):
|
||||||
"""Exception raised when the command being invoked is disabled.
|
"""Exception raised when the command being invoked is disabled.
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ from discord import app_commands
|
|||||||
from discord.utils import MISSING, maybe_coroutine, async_all
|
from discord.utils import MISSING, maybe_coroutine, async_all
|
||||||
from .core import Command, Group
|
from .core import Command, Group
|
||||||
from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError
|
from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError
|
||||||
from .converter import Converter
|
from .converter import Converter, Range
|
||||||
from .parameters import Parameter
|
from .parameters import Parameter
|
||||||
from .cog import Cog
|
from .cog import Cog
|
||||||
|
|
||||||
@ -143,16 +143,17 @@ def replace_parameters(parameters: Dict[str, Parameter], signature: inspect.Sign
|
|||||||
for name, parameter in parameters.items():
|
for name, parameter in parameters.items():
|
||||||
_is_transformer = is_transformer(parameter.converter)
|
_is_transformer = is_transformer(parameter.converter)
|
||||||
origin = getattr(parameter.converter, '__origin__', None)
|
origin = getattr(parameter.converter, '__origin__', None)
|
||||||
if is_converter(parameter.converter) and not _is_transformer:
|
if isinstance(parameter.converter, Range):
|
||||||
|
r = parameter.converter
|
||||||
|
params[name] = params[name].replace(annotation=app_commands.Range[r.annotation, r.min, r.max]) # type: ignore
|
||||||
|
elif is_converter(parameter.converter) and not _is_transformer:
|
||||||
params[name] = params[name].replace(annotation=make_converter_transformer(parameter.converter))
|
params[name] = params[name].replace(annotation=make_converter_transformer(parameter.converter))
|
||||||
|
|
||||||
# Special case Optional[X] where X is a single type that can optionally be a converter
|
|
||||||
elif origin is Union and len(parameter.converter.__args__) == 2 and parameter.converter.__args__[-1] is _NoneType:
|
elif origin is Union and len(parameter.converter.__args__) == 2 and parameter.converter.__args__[-1] is _NoneType:
|
||||||
|
# Special case Optional[X] where X is a single type that can optionally be a converter
|
||||||
inner = parameter.converter.__args__[0]
|
inner = parameter.converter.__args__[0]
|
||||||
is_inner_tranformer = is_transformer(inner)
|
is_inner_tranformer = is_transformer(inner)
|
||||||
if is_converter(inner) and not is_inner_tranformer:
|
if is_converter(inner) and not is_inner_tranformer:
|
||||||
params[name] = params[name].replace(annotation=Optional[make_converter_transformer(inner)]) # type: ignore
|
params[name] = params[name].replace(annotation=Optional[make_converter_transformer(inner)]) # type: ignore
|
||||||
|
|
||||||
elif callable(parameter.converter) and not inspect.isclass(parameter.converter) and not _is_transformer:
|
elif callable(parameter.converter) and not inspect.isclass(parameter.converter) and not _is_transformer:
|
||||||
params[name] = params[name].replace(annotation=make_callable_transformer(parameter.converter))
|
params[name] = params[name].replace(annotation=make_callable_transformer(parameter.converter))
|
||||||
|
|
||||||
|
@ -468,6 +468,8 @@ Converters
|
|||||||
|
|
||||||
.. autoclass:: discord.ext.commands.Greedy()
|
.. autoclass:: discord.ext.commands.Greedy()
|
||||||
|
|
||||||
|
.. autoclass:: discord.ext.commands.Range()
|
||||||
|
|
||||||
.. autofunction:: discord.ext.commands.run_converters
|
.. autofunction:: discord.ext.commands.run_converters
|
||||||
|
|
||||||
Flag Converter
|
Flag Converter
|
||||||
@ -626,6 +628,9 @@ Exceptions
|
|||||||
.. autoexception:: discord.ext.commands.BadBoolArgument
|
.. autoexception:: discord.ext.commands.BadBoolArgument
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoexception:: discord.ext.commands.RangeError
|
||||||
|
:members:
|
||||||
|
|
||||||
.. autoexception:: discord.ext.commands.MissingPermissions
|
.. autoexception:: discord.ext.commands.MissingPermissions
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
@ -713,6 +718,7 @@ Exception Hierarchy
|
|||||||
- :exc:`~.commands.ScheduledEventNotFound`
|
- :exc:`~.commands.ScheduledEventNotFound`
|
||||||
- :exc:`~.commands.PartialEmojiConversionFailure`
|
- :exc:`~.commands.PartialEmojiConversionFailure`
|
||||||
- :exc:`~.commands.BadBoolArgument`
|
- :exc:`~.commands.BadBoolArgument`
|
||||||
|
- :exc:`~.commands.RangeError`
|
||||||
- :exc:`~.commands.ThreadNotFound`
|
- :exc:`~.commands.ThreadNotFound`
|
||||||
- :exc:`~.commands.FlagError`
|
- :exc:`~.commands.FlagError`
|
||||||
- :exc:`~.commands.BadFlagArgument`
|
- :exc:`~.commands.BadFlagArgument`
|
||||||
|
Loading…
x
Reference in New Issue
Block a user