Browse Source

[commands] Add Range converter

This allows hybrid commands to also have a range annotation
pull/10109/head
Rapptz 3 years ago
committed by dolfies
parent
commit
a354ecfa89
  1. 79
      discord/ext/commands/converter.py
  2. 39
      discord/ext/commands/errors.py
  3. 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

@ -100,6 +100,7 @@ __all__ = (
'MissingFlagArgument',
'TooManyFlags',
'MissingRequiredFlag',
'RangeError',
)
@ -553,6 +554,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.

6
docs/ext/commands/api.rst

@ -425,6 +425,8 @@ Converters
.. autoclass:: discord.ext.commands.Greedy()
.. autoclass:: discord.ext.commands.Range()
.. autofunction:: discord.ext.commands.run_converters
Flag Converter
@ -583,6 +585,9 @@ Exceptions
.. autoexception:: discord.ext.commands.BadBoolArgument
:members:
.. autoexception:: discord.ext.commands.RangeError
:members:
.. autoexception:: discord.ext.commands.MissingPermissions
:members:
@ -667,6 +672,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