Skip to content

Commit

Permalink
PEP 613 x 1.
Browse files Browse the repository at this point in the history
This commit is the first in a commit chain adding support for deprecated
**PEP 613 type aliases** (i.e., the `typing.TypeAlias` type hint
singleton), en-route to resolving feature request #328 kindly submitted
by spaghetti-loving Bay Area pasta guru @jamesbraza (James Braza).
Although since deprecated by **PEP 695 type aliases** (e.g., type hints
of the form `type {alias_name} = {alias_value}` under Python � 3.12),
PEP 613 type aliases are still widely prevalent throughout the
open-source community. Specifically, @beartype now:

* Emits one non-fatal `BeartypeDecorHintPep613DeprecationWarning` for
  each PEP 613 type alias resembling:

  ```python
  BeartypeDecorHintPep613DeprecationWarning: PEP 613 type hint
  typing.TypeAlias deprecated by PEP 695. Consider either:
  * Requiring Python >= 3.12 and refactoring PEP 613 type aliases into
    PEP 695 type aliases. Note that Python < 3.12 will hate you for
    this: e.g.,
      # Instead of this...
      from typing import TypeAlias
      alias_name: TypeAlias = alias_value

      # ...just do this.
      type alias_name = alias_value
  * Refactoring PEP 613 type aliases into PEP 484 "typing.NewType"-based
    type aliases. Note that static type-checkers (e.g., mypy, pyright,
    Pyre) will hate you for this: e.g.,
      # Instead of this...
      from typing import TypeAlias
      alias_name: TypeAlias = alias_value

      # ...just do this.
      from typing import NewType
      alias_name = NewType("alias_name", alias_value)

  Combine the above two approaches via The Ultimate Type Alias (TUTA),
  a hidden ninja technique that supports all Python versions and static
  type-checkers but may cause coworker heads to pop off like in that one
  jolly Kingsman scene:
      # Instead of this...
      from typing import TypeAlias
      alias_name: TypeAlias = alias_value

      # ..."just" do this. If you think this sucks, know that you are not alone.
      from typing import TYPE_CHECKING, NewType, TypeAlias  # <-- sus af
      from sys import version_info  # <-- code just got real
      if TYPE_CHECKING:  # <-- if static type-checking, then PEP 613
          alias_name: TypeAlias = alias_value  # <-- grimdark coding style
      elif version_info >= (3, 12):  # <-- if Python >= 3.12, then PEP 695
          exec("type alias_name = alias_value")  # <-- eldritch abomination
      else:  # <-- if Python < 3.12, then PEP 484
          alias_name = NewType("alias_name", alias_value)  # <-- coworker gives up here
  ```
* Otherwise ignores each PEP 613 type alias, which conveys *no*
  meaningful semantics or metadata. Frankly, it's unclear why PEP 613
  even exists. The CPython developer community felt similarly, which is
  why PEP 695 type aliases deprecate PEP 613.

@beartype shrugs noncommittally. (*A hundred red thunders!*)
  • Loading branch information
leycec committed Feb 23, 2024
1 parent adf1a44 commit 9b33afd
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 74 deletions.
24 changes: 16 additions & 8 deletions beartype/_check/convert/convreduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
HintSignPanderaAny,
HintSignPep557DataclassInitVar,
HintSignPep585BuiltinSubscriptedUnknown,
HintSignTypeAlias,
HintSignPep695TypeAlias,
HintSignSelf,
HintSignType,
Expand Down Expand Up @@ -66,6 +67,7 @@
from beartype._util.hint.pep.proposal.utilpep589 import reduce_hint_pep589
from beartype._util.hint.pep.proposal.utilpep591 import reduce_hint_pep591
from beartype._util.hint.pep.proposal.utilpep593 import reduce_hint_pep593
from beartype._util.hint.pep.proposal.utilpep613 import reduce_hint_pep613
from beartype._util.hint.pep.proposal.utilpep647 import reduce_hint_pep647
from beartype._util.hint.pep.proposal.utilpep673 import reduce_hint_pep673
from beartype._util.hint.pep.proposal.utilpep675 import reduce_hint_pep675
Expand Down Expand Up @@ -512,18 +514,24 @@ def reduce_hint_pep{pep_number}(


_HINT_SIGN_TO_REDUCE_HINT_UNCACHED: _HintSignToReduceHintUncached = {
# ..................{ PEP 613 }..................
# Reduce PEP 613-compliant "typing.TypeAlias" type hints to an arbitrary
# ignorable type hint *AND* emit a non-fatal deprecation warning.
#
# Note that, to ensure that one such warning is emitted for each such hint,
# this reducer is intentionally uncached rather than cached.
HintSignTypeAlias: reduce_hint_pep613,

# ..................{ PEP 647 }..................
# If this hint is a PEP 647-compliant "typing.TypeGuard[...]" type hint,
# either:
# * If this hint annotates the return of some callable, reduce this hint to
# the standard "bool" type.
# Reduce PEP 647-compliant "typing.TypeGuard[...]" type hints to either:
# * If this hint annotates the return of some callable, the "bool" type.
# * Else, raise an exception.
HintSignTypeGuard: reduce_hint_pep647,

# ..................{ PEP 673 }..................
# If this hint is a PEP 673-compliant "typing.Self" type hint, either:
# * If @beartype is currently decorating a class, reduce this hint to the
# most deeply nested class on the passed type stack.
# Reduce PEP 673-compliant "typing.Self" type hints to either:
# * If @beartype is currently decorating a class, the most deeply nested
# class on the passed type stack.
# * Else, raise an exception.
HintSignSelf: reduce_hint_pep673,
}
Expand All @@ -534,7 +542,7 @@ def reduce_hint_pep{pep_number}(
*cannot* be efficiently memoized by the :func:`.callable_cached` decorator).
See Also
----------
--------
:data:`._HINT_SIGN_TO_REDUCE_HINT_CACHED`
Further details.
'''
2 changes: 2 additions & 0 deletions beartype/_data/hint/pep/datapeprepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
HintSignParamSpec,
# HintSignParamSpecArgs,
HintSignPep557DataclassInitVar,
HintSignTypeAlias,
HintSignPep695TypeAlias,
# HintSignProtocol,
HintSignReversible,
Expand Down Expand Up @@ -613,6 +614,7 @@ def _init() -> None:
# case, silently continue to the next sign.
if not hint_sign_name.startswith('HintSign'):
continue
# Else, this name is that of a sign.

# Sign with this name.
hint_sign = getattr(datapepsigns, hint_sign_name)
Expand Down
4 changes: 4 additions & 0 deletions beartype/_data/hint/pep/sign/datapepsignset.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
HintSignParamSpec,
HintSignPattern,
HintSignPep585BuiltinSubscriptedUnknown,
HintSignTypeAlias,
HintSignPep695TypeAlias,
HintSignProtocol,
HintSignReversible,
Expand Down Expand Up @@ -507,6 +508,9 @@
# ..................{ PEP 591 }..................
HintSignFinal,

# ..................{ PEP 613 }..................
HintSignTypeAlias,

# ..................{ PEP 647 }..................
HintSignTypeGuard,

Expand Down
17 changes: 14 additions & 3 deletions beartype/_decor/decormain.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
# submodule to improve maintainability and readability here.

# ....................{ IMPORTS }....................
from beartype.typing import TYPE_CHECKING, Callable
from beartype.typing import (
TYPE_CHECKING,
Callable,
)
from beartype._conf.confcls import (
BEARTYPE_CONF_DEFAULT,
BeartypeConf,
Expand All @@ -40,11 +43,19 @@
# @beartype decorator declared below is permissively annotated as returning a
# union of multiple types desynchronized from the types of the passed arguments
# and thus fails to accurately convey the actual public API of that decorator.
# See also: https://www.python.org/dev/peps/pep-0484/#function-method-overloading
# See also:
# https://www.python.org/dev/peps/pep-0484/#function-method-overloading
#
# Note that the "Callable[[BeartypeableT], BeartypeableT]" type hint should
# ideally instead be a reference to our "BeartypeConfedDecorator" type hint.
# Indeed, it used to be. Unfortunately, a significant regression in mypy
# required us to inline that type hint away. See also this issue:
# https://github.com/beartype/beartype/issues/332
@overload # type: ignore[misc,no-overload-impl]
def beartype(obj: BeartypeableT) -> BeartypeableT: ...
@overload
def beartype(*, conf: BeartypeConf) -> Callable[[BeartypeableT], BeartypeableT]: ...
def beartype(*, conf: BeartypeConf) -> Callable[
[BeartypeableT], BeartypeableT]: ...

# ....................{ DECORATORS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
Expand Down
48 changes: 28 additions & 20 deletions beartype/_decor/wrap/wrapmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
# All "FIXME:" comments for this submodule reside in this package's "__init__"
# submodule to improve maintainability and readability here.

#FIXME: Handle "{exception_prefix}" in warnings similar to how we handle that
#for exceptions. Sadly, Python's warning management is considerably more
#cumbersome than its exception management. Still, this is feasible. Moreover, we
#have *NO* choice. Just do it. See also the first solution in this StackOverflow
#answer, which seems quite reasonable:
# https://stackoverflow.com/a/77516994
#FIXME: Perform similar handling in the _make_func_checker() factory defined
#elsewhere, please.

#FIXME: Split this large submodule into smaller submodules for maintainability.
#A useful approach might be:
#* Define a new private "_codearg" submodule and shift the _code_check_args()
Expand Down Expand Up @@ -336,11 +345,11 @@ def _code_check_args(bear_call: BeartypeCall) -> str:
# ..................{ LOCALS ~ hint }..................
# Type hint annotating the current parameter if any *OR* "_PARAM_HINT_EMPTY"
# otherwise (i.e., if this parameter is unannotated).
hint = None
hint_insane = None

# This type hint sanitized into a possibly different type hint more readily
# consumable by @beartype's code generator.
hint_sane = None
hint = None

# ..................{ GENERATE }..................
#FIXME: Locally remove the "arg_index" local variable (and thus avoid
Expand Down Expand Up @@ -370,10 +379,10 @@ def _code_check_args(bear_call: BeartypeCall) -> str:
# Note that "None" is a semantically meaningful PEP 484-compliant type
# hint equivalent to "type(None)". Ergo, we *MUST* explicitly
# distinguish between that type hint and unannotated parameters.
hint = bear_call.func_arg_name_to_hint_get(arg_name, SENTINEL)
hint_insane = bear_call.func_arg_name_to_hint_get(arg_name, SENTINEL)

# If this parameter is unannotated, continue to the next parameter.
if hint is SENTINEL:
if hint_insane is SENTINEL:
continue
# Else, this parameter is annotated.

Expand All @@ -390,22 +399,18 @@ def _code_check_args(bear_call: BeartypeCall) -> str:
continue
# Else, this parameter is non-ignorable.

#FIXME: Definitely the wrong way around. This should simply be
#"hint" -- *NOT* "hint_sane". Define a new "hint_insane = hint"
#alias preserving the original insane hint, please.

# Sanitize this hint into a possibly different type hint more
# readily consumable by @beartype's code generator *BEFORE* passing
# this hint to any further callables.
hint_sane = sanify_hint_root_func(
hint=hint, pith_name=arg_name, bear_call=bear_call)
hint = sanify_hint_root_func(
hint=hint_insane, pith_name=arg_name, bear_call=bear_call)

# If this hint is ignorable, continue to the next parameter.
#
# Note that this is intentionally tested *AFTER* this hint has been
# coerced into a PEP-compliant type hint to implicitly ignore
# PEP-noncompliant type hints as well (e.g., "(object, int, str)").
if is_hint_ignorable(hint_sane):
if is_hint_ignorable(hint):
# print(f'Ignoring {bear_call.func_name} parameter {arg_name} hint {repr(hint)}...')
continue
# Else, this hint is unignorable.
Expand Down Expand Up @@ -447,15 +452,18 @@ def _code_check_args(bear_call: BeartypeCall) -> str:
# Type stack if required by this hint *OR* "None" otherwise. See the
# is_hint_needs_cls_stack() tester for further discussion.
#
# Note that the original unsanitized "hint" (e.g., "typing.Self")
# rather than the new sanitized "hint_sane" (e.g., the class
# currently being decorated by @beartype) is passed to that tester.
# Why? Because the latter may already have been reduced above to a
# different and seemingly innocuous type hint that does *NOT* appear
# to require a type stack but actually does. Only the original
# unsanitized "hint" can tell the truth.
# Note that the original unsanitized "hint_insane" (e.g.,
# "typing.Self") rather than the new sanitized "hint" (e.g., the
# class currently being decorated by @beartype) is passed to that
# tester. Why? Because the latter may already have been reduced
# above to a different and seemingly innocuous type hint that does
# *NOT* appear to require a type stack but actually does. Only the
# original unsanitized "hint_insane" can tell the truth.
cls_stack = (
bear_call.cls_stack if is_hint_needs_cls_stack(hint) else None)
bear_call.cls_stack
if is_hint_needs_cls_stack(hint_insane) else
None
)
# print(f'arg "{arg_name}" hint {repr(hint)} cls_stack: {repr(cls_stack)}')

# Code snippet type-checking any parameter with an arbitrary name.
Expand All @@ -464,7 +472,7 @@ def _code_check_args(bear_call: BeartypeCall) -> str:
func_scope,
hint_refs_type_basename,
) = make_code_raiser_func_pith_check(
hint_sane,
hint,
bear_call.conf,
cls_stack,
True, # <-- True only for parameters
Expand Down
40 changes: 20 additions & 20 deletions beartype/_util/cache/map/utilmaplru.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ def __getitem__(
key: Hashable,

# Superclass methods efficiently localized as default parameters.
__contains=dict.__contains__,
__getitem=dict.__getitem__,
__delitem=dict.__delitem__,
__pushitem=dict.__setitem__,
__contains = dict.__contains__, # pyright: ignore
__getitem = dict.__getitem__, # pyright: ignore
__delitem = dict.__delitem__, # pyright: ignore
__pushitem = dict.__setitem__, # pyright: ignore
) -> object:
'''
Return an item previously cached under the passed key *or* raise an
Expand All @@ -144,12 +144,12 @@ def __getitem__(
Arbitrary hashable key to retrieve the cached value of.
Returns
----------
-------
object
Arbitrary value cached under this key.
Raises
----------
------
TypeError
If this key is not hashable.
KeyError
Expand All @@ -173,11 +173,11 @@ def __setitem__(
value: object,

# Superclass methods efficiently localized as default parameters.
__contains=dict.__contains__,
__delitem=dict.__delitem__,
__pushitem=dict.__setitem__,
__iter=dict.__iter__,
__len=dict.__len__,
__contains = dict.__contains__, # pyright: ignore
__delitem = dict.__delitem__, # pyright: ignore
__pushitem = dict.__setitem__, # pyright: ignore
__iter = dict.__iter__, # pyright: ignore
__len = dict.__len__, # pyright: ignore
) -> None:
'''
Cache this key-value pair while preserving size constraints.
Expand All @@ -190,7 +190,7 @@ def __setitem__(
Arbitrary value to be cached under this key.
Raises
----------
------
TypeError
If this key is not hashable.
'''
Expand All @@ -207,13 +207,13 @@ def __setitem__(

def __contains__(
self,
key: Hashable,
key: Hashable,

# Superclass methods efficiently localized as default parameters.
__contains=dict.__contains__,
__getitem=dict.__getitem__,
__delitem=dict.__delitem__,
__pushitem=dict.__setitem__,
# Superclass methods efficiently localized as default parameters.
__contains = dict.__contains__, # pyright: ignore
__getitem = dict.__getitem__, # pyright: ignore
__delitem = dict.__delitem__, # pyright: ignore
__pushitem = dict.__setitem__, # pyright: ignore
) -> bool:
'''
Return a boolean indicating whether this key is cached.
Expand All @@ -227,9 +227,9 @@ def __contains__(
Arbitrary hashable key to detect the existence of.
Returns
----------
-------
bool
``True`` only if this key is cached.
:data:`True` only if this key is cached.
Raises
----------
Expand Down

0 comments on commit 9b33afd

Please sign in to comment.