|
10 | 10 | import itertools |
11 | 11 | import math |
12 | 12 | import sys |
| 13 | +from abc import ABC, abstractmethod |
| 14 | +from dataclasses import dataclass |
13 | 15 | from enum import Enum, EnumMeta |
14 | 16 | from typing import ( |
15 | 17 | TYPE_CHECKING, |
|
22 | 24 | Generic, |
23 | 25 | List, |
24 | 26 | Literal, |
| 27 | + NoReturn, |
25 | 28 | Optional, |
26 | 29 | Sequence, |
27 | 30 | Tuple, |
|
31 | 34 | get_args, |
32 | 35 | get_origin, |
33 | 36 | get_type_hints, |
34 | | - overload, |
35 | 37 | ) |
36 | 38 |
|
37 | 39 | import disnake |
@@ -279,135 +281,163 @@ def decorator(func: CallableT) -> CallableT: |
279 | 281 | return decorator |
280 | 282 |
|
281 | 283 |
|
282 | | -class RangeMeta(type): |
283 | | - """Custom Generic implementation for Range""" |
| 284 | +@dataclass(frozen=True) |
| 285 | +class _BaseRange(ABC): |
| 286 | + """Internal base type for supporting ``Range[...]`` and ``String[...]``.""" |
284 | 287 |
|
285 | | - @overload |
286 | | - def __getitem__( |
287 | | - self, args: Tuple[Union[int, EllipsisType], Union[int, EllipsisType]] |
288 | | - ) -> Type[int]: |
289 | | - ... |
| 288 | + _allowed_types: ClassVar[Tuple[Type[Any], ...]] |
290 | 289 |
|
291 | | - @overload |
292 | | - def __getitem__( |
293 | | - self, args: Tuple[Union[float, EllipsisType], Union[float, EllipsisType]] |
294 | | - ) -> Type[float]: |
295 | | - ... |
| 290 | + underlying_type: Type[Any] |
| 291 | + min_value: Optional[Union[int, float]] |
| 292 | + max_value: Optional[Union[int, float]] |
296 | 293 |
|
297 | | - def __getitem__(self, args: Tuple[Any, ...]) -> Any: |
298 | | - a, b = [None if isinstance(x, type(Ellipsis)) else x for x in args] |
299 | | - return Range.create(min_value=a, max_value=b) |
| 294 | + def __class_getitem__(cls, params: Tuple[Any, ...]) -> Self: |
| 295 | + # deconstruct type arguments |
| 296 | + if not isinstance(params, tuple): |
| 297 | + params = (params,) |
300 | 298 |
|
| 299 | + name = cls.__name__ |
301 | 300 |
|
302 | | -class Range(type, metaclass=RangeMeta): |
303 | | - """Type depicting a limited range of allowed values. |
| 301 | + if len(params) == 2: |
| 302 | + # backwards compatibility for `Range[1, 2]` |
304 | 303 |
|
305 | | - See :ref:`param_ranges` for more information. |
| 304 | + # FIXME: the warning context is incorrect when used with stringified annotations, |
| 305 | + # and points to the eval frame instead of user code |
| 306 | + disnake.utils.warn_deprecated( |
| 307 | + f"Using `{name}` without an explicit type argument is deprecated, " |
| 308 | + "as this form does not work well with modern type-checkers. " |
| 309 | + f"Use `{name}[<type>, <min>, <max>]` instead.", |
| 310 | + stacklevel=2, |
| 311 | + ) |
306 | 312 |
|
307 | | - .. versionadded:: 2.4 |
| 313 | + # infer type from min/max values |
| 314 | + params = (cls._infer_type(params),) + params |
308 | 315 |
|
309 | | - """ |
| 316 | + if len(params) != 3: |
| 317 | + raise TypeError( |
| 318 | + f"`{name}` expects 3 type arguments ({name}[<type>, <min>, <max>]), got {len(params)}" |
| 319 | + ) |
310 | 320 |
|
311 | | - min_value: Optional[float] |
312 | | - max_value: Optional[float] |
| 321 | + underlying_type, min_value, max_value = params |
313 | 322 |
|
314 | | - @overload |
315 | | - @classmethod |
316 | | - def create( |
317 | | - cls, |
318 | | - min_value: Optional[int] = None, |
319 | | - max_value: Optional[int] = None, |
320 | | - *, |
321 | | - le: Optional[int] = None, |
322 | | - lt: Optional[int] = None, |
323 | | - ge: Optional[int] = None, |
324 | | - gt: Optional[int] = None, |
325 | | - ) -> Type[int]: |
326 | | - ... |
327 | | - |
328 | | - @overload |
329 | | - @classmethod |
330 | | - def create( |
331 | | - cls, |
332 | | - min_value: Optional[float] = None, |
333 | | - max_value: Optional[float] = None, |
334 | | - *, |
335 | | - le: Optional[float] = None, |
336 | | - lt: Optional[float] = None, |
337 | | - ge: Optional[float] = None, |
338 | | - gt: Optional[float] = None, |
339 | | - ) -> Type[float]: |
340 | | - ... |
| 323 | + # validate type (argument 1) |
| 324 | + if not isinstance(underlying_type, type): |
| 325 | + raise TypeError(f"First `{name}` argument must be a type, not `{underlying_type!r}`") |
341 | 326 |
|
342 | | - @classmethod |
343 | | - def create( |
344 | | - cls, |
345 | | - min_value: Optional[float] = None, |
346 | | - max_value: Optional[float] = None, |
347 | | - *, |
348 | | - le: Optional[float] = None, |
349 | | - lt: Optional[float] = None, |
350 | | - ge: Optional[float] = None, |
351 | | - gt: Optional[float] = None, |
352 | | - ) -> Any: |
353 | | - """Construct a new range with any possible constraints""" |
354 | | - self = cls(cls.__name__, (), {}) |
355 | | - self.min_value = min_value if min_value is not None else _xt_to_xe(le, lt, -1) |
356 | | - self.max_value = max_value if max_value is not None else _xt_to_xe(ge, gt, 1) |
357 | | - return self |
| 327 | + if not issubclass(underlying_type, cls._allowed_types): |
| 328 | + allowed = "/".join(t.__name__ for t in cls._allowed_types) |
| 329 | + raise TypeError(f"First `{name}` argument must be {allowed}, not `{underlying_type!r}`") |
358 | 330 |
|
359 | | - @property |
360 | | - def underlying_type(self) -> Union[Type[int], Type[float]]: |
361 | | - if isinstance(self.min_value, float) or isinstance(self.max_value, float): |
362 | | - return float |
| 331 | + # validate min/max (arguments 2/3) |
| 332 | + min_value = cls._coerce_bound(min_value, "min") |
| 333 | + max_value = cls._coerce_bound(max_value, "max") |
| 334 | + |
| 335 | + if min_value is None and max_value is None: |
| 336 | + raise ValueError(f"`{name}` bounds cannot both be empty") |
363 | 337 |
|
364 | | - return int |
| 338 | + # n.b. this allows bounds to be equal, which doesn't really serve a purpose with numbers, |
| 339 | + # but is still accepted by the api |
| 340 | + if min_value is not None and max_value is not None and min_value > max_value: |
| 341 | + raise ValueError( |
| 342 | + f"`{name}` minimum ({min_value}) must be less than or equal to maximum ({max_value})" |
| 343 | + ) |
| 344 | + |
| 345 | + return cls(underlying_type=underlying_type, min_value=min_value, max_value=max_value) |
| 346 | + |
| 347 | + @staticmethod |
| 348 | + def _coerce_bound(value: Any, name: str) -> Optional[Union[int, float]]: |
| 349 | + if value is None or isinstance(value, EllipsisType): |
| 350 | + return None |
| 351 | + elif isinstance(value, (int, float)): |
| 352 | + if not math.isfinite(value): |
| 353 | + raise ValueError(f"{name} value may not be NaN, inf, or -inf") |
| 354 | + return value |
| 355 | + else: |
| 356 | + raise TypeError(f"{name} value must be int, float, None, or `...`, not `{type(value)}`") |
365 | 357 |
|
366 | 358 | def __repr__(self) -> str: |
367 | 359 | a = "..." if self.min_value is None else self.min_value |
368 | 360 | b = "..." if self.max_value is None else self.max_value |
369 | | - return f"{type(self).__name__}[{a}, {b}]" |
| 361 | + return f"{type(self).__name__}[{self.underlying_type.__name__}, {a}, {b}]" |
370 | 362 |
|
| 363 | + @classmethod |
| 364 | + @abstractmethod |
| 365 | + def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: |
| 366 | + raise NotImplementedError |
371 | 367 |
|
372 | | -class StringMeta(type): |
373 | | - """Custom Generic implementation for String.""" |
| 368 | + # hack to get `typing._type_check` to pass, e.g. when using `Range` as a generic parameter |
| 369 | + def __call__(self) -> NoReturn: |
| 370 | + raise NotImplementedError |
374 | 371 |
|
375 | | - def __getitem__( |
376 | | - self, args: Tuple[Union[int, EllipsisType], Union[int, EllipsisType]] |
377 | | - ) -> Type[str]: |
378 | | - a, b = [None if isinstance(x, EllipsisType) else x for x in args] |
379 | | - return String.create(min_length=a, max_length=b) |
| 372 | + # support new union syntax for `Range[int, 1, 2] | None` |
| 373 | + if sys.version_info >= (3, 10): |
380 | 374 |
|
| 375 | + def __or__(self, other): |
| 376 | + return Union[self, other] # type: ignore |
381 | 377 |
|
382 | | -class String(type, metaclass=StringMeta): |
383 | | - """Type depicting a string option with limited length. |
384 | 378 |
|
385 | | - See :ref:`string_lengths` for more information. |
| 379 | +if TYPE_CHECKING: |
| 380 | + # aliased import since mypy doesn't understand `Range = Annotated` |
| 381 | + from typing_extensions import Annotated as Range, Annotated as String |
| 382 | +else: |
386 | 383 |
|
387 | | - .. versionadded:: 2.6 |
| 384 | + @dataclass(frozen=True, repr=False) |
| 385 | + class Range(_BaseRange): |
| 386 | + """Type representing a number with a limited range of allowed values. |
388 | 387 |
|
389 | | - """ |
| 388 | + See :ref:`param_ranges` for more information. |
390 | 389 |
|
391 | | - min_length: Optional[int] |
392 | | - max_length: Optional[int] |
393 | | - underlying_type: Final[Type[str]] = str |
| 390 | + .. versionadded:: 2.4 |
394 | 391 |
|
395 | | - @classmethod |
396 | | - def create( |
397 | | - cls, |
398 | | - min_length: Optional[int] = None, |
399 | | - max_length: Optional[int] = None, |
400 | | - ) -> Any: |
401 | | - """Construct a new String with constraints.""" |
402 | | - self = cls(cls.__name__, (), {}) |
403 | | - self.min_length = min_length |
404 | | - self.max_length = max_length |
405 | | - return self |
| 392 | + .. versionchanged:: 2.9 |
| 393 | + Syntax changed from ``Range[5, 10]`` to ``Range[int, 5, 10]``; |
| 394 | + the type (:class:`int` or :class:`float`) must now be specified explicitly. |
| 395 | + """ |
406 | 396 |
|
407 | | - def __repr__(self) -> str: |
408 | | - a = "..." if self.min_length is None else self.min_length |
409 | | - b = "..." if self.max_length is None else self.max_length |
410 | | - return f"{type(self).__name__}[{a}, {b}]" |
| 397 | + _allowed_types = (int, float) |
| 398 | + |
| 399 | + def __post_init__(self): |
| 400 | + for value in (self.min_value, self.max_value): |
| 401 | + if value is None: |
| 402 | + continue |
| 403 | + |
| 404 | + if self.underlying_type is int and not isinstance(value, int): |
| 405 | + raise TypeError("Range[int, ...] bounds must be int, not float") |
| 406 | + |
| 407 | + @classmethod |
| 408 | + def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: |
| 409 | + if any(isinstance(p, float) for p in params): |
| 410 | + return float |
| 411 | + return int |
| 412 | + |
| 413 | + @dataclass(frozen=True, repr=False) |
| 414 | + class String(_BaseRange): |
| 415 | + """Type representing a string option with a limited length. |
| 416 | +
|
| 417 | + See :ref:`string_lengths` for more information. |
| 418 | +
|
| 419 | + .. versionadded:: 2.6 |
| 420 | +
|
| 421 | + .. versionchanged:: 2.9 |
| 422 | + Syntax changed from ``String[5, 10]`` to ``String[str, 5, 10]``; |
| 423 | + the type (:class:`str`) must now be specified explicitly. |
| 424 | + """ |
| 425 | + |
| 426 | + _allowed_types = (str,) |
| 427 | + |
| 428 | + def __post_init__(self): |
| 429 | + for value in (self.min_value, self.max_value): |
| 430 | + if value is None: |
| 431 | + continue |
| 432 | + |
| 433 | + if not isinstance(value, int): |
| 434 | + raise TypeError("String bounds must be int, not float") |
| 435 | + if value < 0: |
| 436 | + raise ValueError("String bounds may not be negative") |
| 437 | + |
| 438 | + @classmethod |
| 439 | + def _infer_type(cls, params: Tuple[Any, ...]) -> Type[Any]: |
| 440 | + return str |
411 | 441 |
|
412 | 442 |
|
413 | 443 | class LargeInt(int): |
@@ -701,14 +731,14 @@ def parse_annotation(self, annotation: Any, converter_mode: bool = False) -> boo |
701 | 731 | if annotation is inspect.Parameter.empty or annotation is Any: |
702 | 732 | return False |
703 | 733 |
|
704 | | - # resolve type aliases |
| 734 | + # resolve type aliases and special types |
705 | 735 | if isinstance(annotation, Range): |
706 | 736 | self.min_value = annotation.min_value |
707 | 737 | self.max_value = annotation.max_value |
708 | 738 | annotation = annotation.underlying_type |
709 | 739 | if isinstance(annotation, String): |
710 | | - self.min_length = annotation.min_length |
711 | | - self.max_length = annotation.max_length |
| 740 | + self.min_length = annotation.min_value |
| 741 | + self.max_length = annotation.max_value |
712 | 742 | annotation = annotation.underlying_type |
713 | 743 | if issubclass_(annotation, LargeInt): |
714 | 744 | self.large = True |
|
0 commit comments