Browse Source

[commands] Add Range converter

This allows hybrid commands to also have a range annotation
pull/7881/head
Rapptz 3 years ago
parent
commit
896932faf1
  1. 79
      discord/ext/commands/converter.py
  2. 39
      discord/ext/commands/errors.py
  3. 11
      discord/ext/commands/hybrid.py
  4. 6
      docs/ext/commands/api.rst

79
discord/ext/commands/converter.py

@ -81,6 +81,7 @@ __all__ = (
'ScheduledEventConverter',
'clean_content',
'Greedy',
'Range',
'run_converters',
)
@ -1053,6 +1054,84 @@ class Greedy(List[T]):
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:
lowered = argument.lower()
if lowered in ('yes', 'y', 'true', 't', '1', 'enable', 'on'):

39
discord/ext/commands/errors.py

@ -102,6 +102,7 @@ __all__ = (
'TooManyFlags',
'MissingRequiredFlag',
'HybridCommandError',
'RangeError',
)
@ -555,6 +556,44 @@ class BadBoolArgument(BadArgument):
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):
"""Exception raised when the command being invoked is disabled.

11
discord/ext/commands/hybrid.py

@ -43,7 +43,7 @@ from discord import app_commands
from discord.utils import MISSING, maybe_coroutine, async_all
from .core import Command, Group
from .errors import BadArgument, CommandRegistrationError, CommandError, HybridCommandError, ConversionError
from .converter import Converter
from .converter import Converter, Range
from .parameters import Parameter
from .cog import Cog
@ -143,16 +143,17 @@ def replace_parameters(parameters: Dict[str, Parameter], signature: inspect.Sign
for name, parameter in parameters.items():
_is_transformer = is_transformer(parameter.converter)
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))
# 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:
# Special case Optional[X] where X is a single type that can optionally be a converter
inner = parameter.converter.__args__[0]
is_inner_tranformer = is_transformer(inner)
if is_converter(inner) and not is_inner_tranformer:
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:
params[name] = params[name].replace(annotation=make_callable_transformer(parameter.converter))

6
docs/ext/commands/api.rst

@ -468,6 +468,8 @@ Converters
.. autoclass:: discord.ext.commands.Greedy()
.. autoclass:: discord.ext.commands.Range()
.. autofunction:: discord.ext.commands.run_converters
Flag Converter
@ -626,6 +628,9 @@ Exceptions
.. autoexception:: discord.ext.commands.BadBoolArgument
:members:
.. autoexception:: discord.ext.commands.RangeError
:members:
.. autoexception:: discord.ext.commands.MissingPermissions
:members:
@ -713,6 +718,7 @@ Exception Hierarchy
- :exc:`~.commands.ScheduledEventNotFound`
- :exc:`~.commands.PartialEmojiConversionFailure`
- :exc:`~.commands.BadBoolArgument`
- :exc:`~.commands.RangeError`
- :exc:`~.commands.ThreadNotFound`
- :exc:`~.commands.FlagError`
- :exc:`~.commands.BadFlagArgument`

Loading…
Cancel
Save