diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 0fa617c37..81a413fb0 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -1041,8 +1041,8 @@ if TYPE_CHECKING: else: class Range: - """A special converter that can be applied to a parameter to require a numeric type - to fit within the range provided. + """A special converter that can be applied to a parameter to require a numeric + or string 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. @@ -1055,6 +1055,9 @@ else: Inside a :class:`HybridCommand` this functions equivalently to :class:`discord.app_commands.Range`. + If the converter fails then :class:`~.ext.commands.RangeError` is raised to + the appropriate error handlers. + .. versionadded:: 2.0 Examples @@ -1079,8 +1082,11 @@ else: 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): + count = converted = self.annotation(value) + if self.annotation is str: + count = len(value) + + if (self.min is not None and count < self.min) or (self.max is not None and count > self.max): raise RangeError(converted, minimum=self.min, maximum=self.max) return converted @@ -1108,13 +1114,18 @@ else: 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') + if annotation not in (int, float, str): + raise TypeError(f'expected int, float, or str as range type, received {annotation!r} instead') + + if annotation in (str, int): + cast = int + else: + cast = float return cls( annotation=annotation, - min=annotation(min) if min is not None else None, - max=annotation(max) if max is not None else None, + min=cast(min) if min is not None else None, + max=cast(max) if max is not None else None, ) diff --git a/discord/ext/commands/errors.py b/discord/ext/commands/errors.py index 6587aa0d7..b8228cf06 100644 --- a/discord/ext/commands/errors.py +++ b/discord/ext/commands/errors.py @@ -594,17 +594,17 @@ class RangeError(BadArgument): 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`] + value: Union[:class:`int`, :class:`float`, :class:`str`] The value that was out of range. """ def __init__( self, - value: Union[int, float], + value: Union[int, float, str], minimum: Optional[Union[int, float]], maximum: Optional[Union[int, float]], ) -> None: - self.value: Union[int, float] = value + self.value: Union[int, float, str] = value self.minimum: Optional[Union[int, float]] = minimum self.maximum: Optional[Union[int, float]] = maximum @@ -616,6 +616,14 @@ class RangeError(BadArgument): elif maximum is not None and minimum is not None: label = f'between {minimum} and {maximum}' + if label and isinstance(value, str): + label += ' characters' + count = len(value) + if count == 1: + value = '1 character' + else: + value = f'{count} characters' + super().__init__(f'value must be {label} but received {value}')