From 2347498b2a3f3ceb5d1fb7ce502646872af4a7b1 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:21:23 +0200 Subject: [PATCH 01/10] Add support for `Range[LargeInt, ...]` --- disnake/app_commands.py | 4 +- disnake/ext/commands/errors.py | 31 ++++++++ disnake/ext/commands/params.py | 136 ++++++++++++++++++++++---------- test_bot/cogs/slash_commands.py | 14 ++-- 4 files changed, 135 insertions(+), 50 deletions(-) diff --git a/disnake/app_commands.py b/disnake/app_commands.py index 727f35cb93..03f8633686 100644 --- a/disnake/app_commands.py +++ b/disnake/app_commands.py @@ -283,9 +283,9 @@ def __init__( self.required: bool = required self.options: List[Option] = options or [] - if min_value and self.type is OptionType.integer: + if min_value is not None and self.type is OptionType.integer: min_value = math.ceil(min_value) - if max_value and self.type is OptionType.integer: + if max_value is not None and self.type is OptionType.integer: max_value = math.floor(max_value) self.min_value: Optional[float] = min_value diff --git a/disnake/ext/commands/errors.py b/disnake/ext/commands/errors.py index cfa4c12f03..6d50400fb2 100644 --- a/disnake/ext/commands/errors.py +++ b/disnake/ext/commands/errors.py @@ -553,6 +553,37 @@ def __init__(self, argument: str) -> None: super().__init__(f"{argument} is not able to be converted to an integer") +class LargeIntOutOfRange(BadArgument): + """Exception raised when an argument to a large integer option exceeds given range. + + This inherits from :exc:`BadArgument` + + .. versionadded:: 2.11 + + Attributes + ---------- + argument: :class:`str` + The argument that exceeded the defined range. + min_value: Optional[Union[:class:`int`, :class:`float`]] + The minimum allowed value. + max_value: Optional[Union[:class:`int`, :class:`float`]] + The maximum allowed value. + """ + + def __init__( + self, + argument: str, + min_value: Union[int, float, None], + max_value: Union[int, float, None], + ) -> None: + self.argument: str = argument + self.min_value: Union[int, float, None] = min_value + self.max_value: Union[int, float, None] = max_value + a = "..." if min_value is None else min_value + b = "..." if max_value is None else max_value + super().__init__(f"{argument} is not in range [{a}, {b}]") + + class DisabledCommand(CommandError): """Exception raised when the command being invoked is disabled. diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index e472c1ae13..c8957b3e3d 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -135,7 +135,7 @@ def issubclass_(obj: Any, tp: Union[TypeT, Tuple[TypeT, ...]]) -> TypeGuard[Type def remove_optionals(annotation: Any) -> Any: - """Remove unwanted optionals from an annotation""" + """Remove unwanted optionals from an annotation.""" if get_origin(annotation) in (Union, UnionType): args = tuple(i for i in annotation.__args__ if i not in (None, type(None))) if len(args) == 1: @@ -163,6 +163,29 @@ def _xt_to_xe(xe: Optional[float], xt: Optional[float], direction: float = 1) -> return None +def _int_to_str_len(number: int) -> int: + """Returns `len(str(number))`, i.e. character count of base 10 signed repr of `number`.""" + # Desmos equivalent: floor(log(max(abs(x), 1))) + 1 + max(-sign(x), 0) + return ( + int(math.log10(abs(number) or 1)) + # 0 -> 0, 1 -> 0, 9 -> 0, 10 -> 1 + + 1 + + (number < 0) + ) + + +def _range_to_str_len(min_value: int, max_value: int) -> Tuple[int, int]: + min_ = _int_to_str_len(min_value) + max_ = _int_to_str_len(max_value) + opposite_sign = (min_value < 0) ^ (max_value < 0) + # both bounds positive: len(str(min_value)) <= len(str(max_value)) + # smaller bound negative: the range includes 0, which sets the minimum length to 1 + # both bounds negative: len(str(min_value)) >= len(str(max_value)) + if opposite_sign: + return 1, max(min_, max_) + return min(min_, max_), max(min_, max_) + + class Injection(Generic[P, T_]): """Represents a slash command injection. @@ -262,17 +285,24 @@ def decorator(func: CallableT) -> CallableT: return decorator +NumberT = TypeVar("NumberT", bound=Union[int, float]) + + @dataclass(frozen=True) -class _BaseRange(ABC): +class _BaseRange(ABC, Generic[NumberT]): """Internal base type for supporting ``Range[...]`` and ``String[...]``.""" - _allowed_types: ClassVar[Tuple[Type[Any], ...]] + _allowed_types: ClassVar[Tuple[type, ...]] - underlying_type: Type[Any] - min_value: Optional[Union[int, float]] - max_value: Optional[Union[int, float]] + underlying_type: type + min_value: Optional[NumberT] + max_value: Optional[NumberT] def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: + if cls is _BaseRange: + # needed since made generic + return super().__class_getitem__(params) # pyright: ignore[reportAttributeAccessIssue] + # deconstruct type arguments if not isinstance(params, tuple): params = (params,) @@ -290,13 +320,12 @@ def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: f"Use `{name}[, , ]` instead.", stacklevel=2, ) - # infer type from min/max values params = (cls._infer_type(params),) + params if len(params) != 3: raise TypeError( - f"`{name}` expects 3 type arguments ({name}[, , ]), got {len(params)}" + f"`{name}` expects 3 arguments ({name}[, , ]), got {len(params)}" ) underlying_type, min_value, max_value = params @@ -305,7 +334,7 @@ def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: if not isinstance(underlying_type, type): raise TypeError(f"First `{name}` argument must be a type, not `{underlying_type!r}`") - if not issubclass(underlying_type, cls._allowed_types): + if not issubclass_(underlying_type, cls._allowed_types): allowed = "/".join(t.__name__ for t in cls._allowed_types) raise TypeError(f"First `{name}` argument must be {allowed}, not `{underlying_type!r}`") @@ -326,7 +355,7 @@ def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: return cls(underlying_type=underlying_type, min_value=min_value, max_value=max_value) @staticmethod - def _coerce_bound(value: Any, name: str) -> Optional[Union[int, float]]: + def _coerce_bound(value: Union[NumberT, None, EllipsisType], name: str) -> Optional[NumberT]: if value is None or isinstance(value, EllipsisType): return None elif isinstance(value, (int, float)): @@ -341,9 +370,9 @@ def __repr__(self) -> str: b = "..." if self.max_value is None else self.max_value return f"{type(self).__name__}[{self.underlying_type.__name__}, {a}, {b}]" - @classmethod + @staticmethod @abstractmethod - def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: + def _infer_type(params: Tuple[Any, ...]) -> Type[Any]: raise NotImplementedError # hack to get `typing._type_check` to pass, e.g. when using `Range` as a generic parameter @@ -353,8 +382,8 @@ def __call__(self) -> NoReturn: # support new union syntax for `Range[int, 1, 2] | None` if sys.version_info >= (3, 10): - def __or__(self, other): - return Union[self, other] # type: ignore + def __or__(self, other: type) -> UnionType: + return Union[self, other] # pyright: ignore if TYPE_CHECKING: @@ -363,7 +392,7 @@ def __or__(self, other): else: @dataclass(frozen=True, repr=False) - class Range(_BaseRange): + class Range(_BaseRange[Union[int, float]]): """Type representing a number with a limited range of allowed values. See :ref:`param_ranges` for more information. @@ -377,22 +406,30 @@ class Range(_BaseRange): _allowed_types = (int, float) - def __post_init__(self): + def __post_init__(self) -> None: for value in (self.min_value, self.max_value): if value is None: continue - if self.underlying_type is int and not isinstance(value, int): + if self.underlying_type is not float and not isinstance(value, int): raise TypeError("Range[int, ...] bounds must be int, not float") - @classmethod - def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: + if self.underlying_type is int and abs(value) >= 2**53: + raise ValueError( + "Discord has upper input limit on integer input type of +/-2**53.\n" + " For larger values, use Range[commands.LargeInt, ...], which will use" + " string input type with length limited to the minimum and maximum string" + " representations of the range bounds." + ) + + @staticmethod + def _infer_type(params: Tuple[Any, ...]) -> Type[Any]: if any(isinstance(p, float) for p in params): return float return int @dataclass(frozen=True, repr=False) - class String(_BaseRange): + class String(_BaseRange[int]): """Type representing a string option with a limited length. See :ref:`string_lengths` for more information. @@ -406,7 +443,7 @@ class String(_BaseRange): _allowed_types = (str,) - def __post_init__(self): + def __post_init__(self) -> None: for value in (self.min_value, self.max_value): if value is None: continue @@ -416,13 +453,13 @@ def __post_init__(self): if value < 0: raise ValueError("String bounds may not be negative") - @classmethod - def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: + @staticmethod + def _infer_type(params: Tuple[Any, ...]) -> Type[Any]: return str class LargeInt(int): - """Type for large integers in slash commands.""" + """Type representing integers `=<-2**53`, `>=2**53` in slash commands.""" # option types that require additional handling in verify_type @@ -478,25 +515,23 @@ class ParamInfo: """ TYPES: ClassVar[Dict[type, int]] = { - # fmt: off str: OptionType.string.value, int: OptionType.integer.value, bool: OptionType.boolean.value, + float: OptionType.number.value, disnake.abc.User: OptionType.user.value, disnake.User: OptionType.user.value, disnake.Member: OptionType.user.value, Union[disnake.User, disnake.Member]: OptionType.user.value, - # channels handled separately - disnake.abc.GuildChannel: OptionType.channel.value, disnake.Role: OptionType.role.value, disnake.abc.Snowflake: OptionType.mentionable.value, Union[disnake.Member, disnake.Role]: OptionType.mentionable.value, Union[disnake.User, disnake.Role]: OptionType.mentionable.value, Union[disnake.User, disnake.Member, disnake.Role]: OptionType.mentionable.value, - float: OptionType.number.value, disnake.Attachment: OptionType.attachment.value, - # fmt: on - } + # channels handled separately + disnake.abc.GuildChannel: OptionType.channel.value, + } # fmt: skip _registered_converters: ClassVar[Dict[type, Callable]] = {} def __init__( @@ -511,10 +546,10 @@ def __init__( choices: Optional[Choices] = None, type: Optional[type] = None, channel_types: Optional[List[ChannelType]] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, + lt: Union[int, float, None] = None, + le: Union[int, float, None] = None, + gt: Union[int, float, None] = None, + ge: Union[int, float, None] = None, large: bool = False, min_length: Optional[int] = None, max_length: Optional[int] = None, @@ -535,10 +570,10 @@ def __init__( self.choices = choices or [] self.type = type or str self.channel_types = channel_types or [] - self.max_value = _xt_to_xe(le, lt, -1) - self.min_value = _xt_to_xe(ge, gt, 1) - self.min_length = min_length - self.max_length = max_length + self.min_value: Union[int, float, None] = _xt_to_xe(ge, gt, 1) + self.max_value: Union[int, float, None] = _xt_to_xe(le, lt, -1) + self.min_length: Optional[int] = min_length + self.max_length: Optional[int] = max_length self.large = large def copy(self) -> Self: @@ -619,7 +654,7 @@ def __repr__(self) -> str: return f"{type(self).__name__}({args})" async def get_default(self, inter: ApplicationCommandInteraction) -> Any: - """Gets the default for an interaction""" + """Gets the default for an interaction.""" default = self.default if callable(self.default): default = self.default(inter) @@ -651,13 +686,19 @@ async def verify_type(self, inter: ApplicationCommandInteraction, argument: Any) return argument async def convert_argument(self, inter: ApplicationCommandInteraction, argument: Any) -> Any: - """Convert a value if a converter is given""" + """Convert a value if a converter is given.""" if self.large: try: argument = int(argument) except ValueError: raise errors.LargeIntConversionFailure(argument) from None + min_value = -math.inf if self.min_value is None else self.min_value + max_value = math.inf if self.max_value is None else self.max_value + + if not min_value <= argument <= max_value: + raise errors.LargeIntOutOfRange(argument, self.min_value, self.max_value) from None + if self.converter is None: # TODO: Custom validators return await self.verify_type(inter, argument) @@ -717,10 +758,12 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo self.min_value = annotation.min_value self.max_value = annotation.max_value annotation = annotation.underlying_type - if isinstance(annotation, String): + + elif isinstance(annotation, String): self.min_length = annotation.min_value self.max_length = annotation.max_value annotation = annotation.underlying_type + if issubclass_(annotation, LargeInt): self.large = True annotation = int @@ -729,6 +772,13 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo self.type = str if annotation is not int: raise TypeError("Large integers must be annotated with int or LargeInt") + + # if either bound is None or ..., we cannot restrict the length + if self.min_value is not None and self.max_value is not None: + self.min_length, self.max_length = _range_to_str_len( + self.min_value, self.max_value # pyright: ignore + ) + elif annotation in self.TYPES: self.type = annotation elif ( @@ -827,8 +877,8 @@ def to_option(self) -> Option: choices=self.choices or None, channel_types=self.channel_types, autocomplete=self.autocomplete is not None, - min_value=self.min_value, - max_value=self.max_value, + min_value=None if self.large else self.min_value, + max_value=None if self.large else self.max_value, min_length=self.min_length, max_length=self.max_length, ) diff --git a/test_bot/cogs/slash_commands.py b/test_bot/cogs/slash_commands.py index e7a2437d56..86f1460c75 100644 --- a/test_bot/cogs/slash_commands.py +++ b/test_bot/cogs/slash_commands.py @@ -6,7 +6,7 @@ from disnake.ext import commands -async def test_autocomp(inter, string: str): +async def test_alt_autocomp(inter: disnake.CommandInteraction[commands.Bot], string: str): return ["XD", ":D", ":)", ":|", ":("] @@ -36,7 +36,7 @@ async def test_autocomp(self, inter: disnake.CommandInteraction[commands.Bot], s async def alt_auto( self, inter: disnake.AppCmdInter[commands.Bot], - mood: str = commands.Param(autocomplete=test_autocomp), + mood: str = commands.Param(autocomplete=test_alt_autocomp), ) -> None: await inter.send(mood) @@ -68,10 +68,14 @@ async def ranges( @commands.slash_command() async def largenumber( - self, inter: disnake.CommandInteraction[commands.Bot], largenum: commands.LargeInt + self, + inter: disnake.CommandInteraction[commands.Bot], + a: commands.LargeInt, + b: commands.Range[commands.LargeInt, -(2**54), 2000], + c: Optional[commands.Range[commands.LargeInt, ..., 2000]] = None, ) -> None: - await inter.send(f"Is int: {isinstance(largenum, int)}") + await inter.send(f"{inter.options}\n{a} {b} {c}") -def setup(bot) -> None: +def setup(bot: commands.Bot) -> None: bot.add_cog(SlashCommands(bot)) From 7b48672b3153aa35072df9d03b5d604df6d4ea1e Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:24:20 +0200 Subject: [PATCH 02/10] Use wider type ignore --- disnake/ext/commands/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index c8957b3e3d..c0853beac2 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -301,7 +301,7 @@ class _BaseRange(ABC, Generic[NumberT]): def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: if cls is _BaseRange: # needed since made generic - return super().__class_getitem__(params) # pyright: ignore[reportAttributeAccessIssue] + return super().__class_getitem__(params) # pyright: ignore # deconstruct type arguments if not isinstance(params, tuple): From 3bdde3d88b5b9c532473a0f244d4044ac404792a Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Tue, 18 Jun 2024 18:27:08 +0200 Subject: [PATCH 03/10] =?UTF-8?q?Fix=20typo:=20=3D<=20=E2=86=92=20<=3D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- disnake/ext/commands/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index c0853beac2..caf6af4788 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -459,7 +459,7 @@ def _infer_type(params: Tuple[Any, ...]) -> Type[Any]: class LargeInt(int): - """Type representing integers `=<-2**53`, `>=2**53` in slash commands.""" + """Type representing integers `<=-2**53`, `>=2**53` in slash commands.""" # option types that require additional handling in verify_type From 2af4466ba968bc15f27581190ad9616429c72b02 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:18:12 +0100 Subject: [PATCH 04/10] Fix 3.8/3.9 ellipsis issues --- disnake/ext/commands/params.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index caf6af4788..d6ce28807f 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -85,10 +85,9 @@ P = TypeVar("P") if sys.version_info >= (3, 10): - from types import EllipsisType, UnionType + from types import UnionType else: - UnionType = object() - EllipsisType = type(Ellipsis) + UnionType = Type[Union] T = TypeVar("T", bound=Any) TypeT = TypeVar("TypeT", bound=Type[Any]) @@ -355,8 +354,8 @@ def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: return cls(underlying_type=underlying_type, min_value=min_value, max_value=max_value) @staticmethod - def _coerce_bound(value: Union[NumberT, None, EllipsisType], name: str) -> Optional[NumberT]: - if value is None or isinstance(value, EllipsisType): + def _coerce_bound(value: Optional[NumberT], name: str) -> Optional[NumberT]: + if value is None or value is ...: return None elif isinstance(value, (int, float)): if not math.isfinite(value): From ea39c9a6cbe80a98a765b301b60cd215ab621c16 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Wed, 13 Nov 2024 20:56:40 +0100 Subject: [PATCH 05/10] Set string length for certain ranges with one unbound --- disnake/ext/commands/params.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index d6ce28807f..43a56a2a88 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -768,16 +768,25 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo annotation = int if self.large: - self.type = str if annotation is not int: raise TypeError("Large integers must be annotated with int or LargeInt") + self.type = str - # if either bound is None or ..., we cannot restrict the length if self.min_value is not None and self.max_value is not None: + # honestly would rather assert than type ignore these self.min_length, self.max_length = _range_to_str_len( - self.min_value, self.max_value # pyright: ignore + self.min_value, # pyright: ignore + self.max_value, # pyright: ignore ) + elif self.min_value is not None and self.min_value > 0: + # 0 < min_value <= max_value == inf + self.min_length = _int_to_str_len(self.min_value) # pyright: ignore + + elif self.max_value is not None and self.max_value < 0: + # -inf == max_value <= min_value < 0 + self.max_length = _int_to_str_len(self.max_value) # pyright: ignore + elif annotation in self.TYPES: self.type = annotation elif ( From bbf2681e7b4f7f2e7e49740cfd40ea081d991a4d Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:52:44 +0100 Subject: [PATCH 06/10] Changelog --- changelog/1201.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1201.feature.rst diff --git a/changelog/1201.feature.rst b/changelog/1201.feature.rst new file mode 100644 index 0000000000..31597f0409 --- /dev/null +++ b/changelog/1201.feature.rst @@ -0,0 +1 @@ +|commands| Add support for ``Range[LargeInt, ...]`` in command parameters \ No newline at end of file From 1fae339fab06d20592a01f926a1494ab243e1d72 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Thu, 14 Nov 2024 00:58:51 +0100 Subject: [PATCH 07/10] Fix test regex --- tests/ext/commands/test_params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ext/commands/test_params.py b/tests/ext/commands/test_params.py index 96f2c08c32..6e36df8050 100644 --- a/tests/ext/commands/test_params.py +++ b/tests/ext/commands/test_params.py @@ -71,7 +71,7 @@ async def test_verify_type__invalid_member(self, annotation, arg_types) -> None: class TestBaseRange: @pytest.mark.parametrize("args", [int, (int,), (int, 1, 2, 3)]) def test_param_count(self, args) -> None: - with pytest.raises(TypeError, match=r"`Range` expects 3 type arguments"): + with pytest.raises(TypeError, match=r"`Range` expects 3 arguments"): commands.Range[args] # type: ignore @pytest.mark.parametrize("value", ["int", 42, Optional[int], Union[int, float]]) From f13aad0acb3ed525983c87447a0103700d0fc219 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:02:17 +0100 Subject: [PATCH 08/10] Revert UnionType def --- disnake/ext/commands/params.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index 43a56a2a88..f5f8a04a11 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -87,7 +87,7 @@ if sys.version_info >= (3, 10): from types import UnionType else: - UnionType = Type[Union] + UnionType = object() T = TypeVar("T", bound=Any) TypeT = TypeVar("TypeT", bound=Type[Any]) From 3b41b8ba87710d35be5843d2c28d30b3155b8ea3 Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:03:19 +0100 Subject: [PATCH 09/10] Oh no, missing newline! --- changelog/1201.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/1201.feature.rst b/changelog/1201.feature.rst index 31597f0409..f3b45cff30 100644 --- a/changelog/1201.feature.rst +++ b/changelog/1201.feature.rst @@ -1 +1 @@ -|commands| Add support for ``Range[LargeInt, ...]`` in command parameters \ No newline at end of file +|commands| Add support for ``Range[LargeInt, ...]`` in command parameters From 5fff87a853efbef2a4e5e1aa62dba0bc5359e62b Mon Sep 17 00:00:00 2001 From: Eneg <42005170+Enegg@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:51:15 +0100 Subject: [PATCH 10/10] Extract function --- disnake/ext/commands/params.py | 36 ++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/disnake/ext/commands/params.py b/disnake/ext/commands/params.py index f5f8a04a11..ffe00e97ae 100644 --- a/disnake/ext/commands/params.py +++ b/disnake/ext/commands/params.py @@ -185,6 +185,23 @@ def _range_to_str_len(min_value: int, max_value: int) -> Tuple[int, int]: return min(min_, max_), max(min_, max_) +def _unbound_range_to_str_len( + min_value: Optional[int], max_value: Optional[int] +) -> Tuple[Optional[int], Optional[int]]: + if min_value is not None and max_value is not None: + return _range_to_str_len(min_value, max_value) + + elif min_value is not None and min_value > 0: + # 0 < min_value <= max_value == inf + return _int_to_str_len(min_value), None + + elif max_value is not None and max_value < 0: + # -inf == min_value <= max_value < 0 + return None, _int_to_str_len(max_value) + + return None, None + + class Injection(Generic[P, T_]): """Represents a slash command injection. @@ -771,21 +788,10 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo if annotation is not int: raise TypeError("Large integers must be annotated with int or LargeInt") self.type = str - - if self.min_value is not None and self.max_value is not None: - # honestly would rather assert than type ignore these - self.min_length, self.max_length = _range_to_str_len( - self.min_value, # pyright: ignore - self.max_value, # pyright: ignore - ) - - elif self.min_value is not None and self.min_value > 0: - # 0 < min_value <= max_value == inf - self.min_length = _int_to_str_len(self.min_value) # pyright: ignore - - elif self.max_value is not None and self.max_value < 0: - # -inf == max_value <= min_value < 0 - self.max_length = _int_to_str_len(self.max_value) # pyright: ignore + self.min_length, self.max_length = _unbound_range_to_str_len( + self.min_value, # type: ignore + self.max_value, # type: ignore + ) elif annotation in self.TYPES: self.type = annotation