Skip to content

Commit

Permalink
"typing_extensions.Annotated" x 2.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain adding transparent support
for the third-party `typing_extensions.Annotated` type hint back-ported
to Python < 3.9, en route to resolving #34. Once finalized, this will
enable usage of beartype validators under Python < 3.9 via this hint.
Specifically, this commit generalizes the existing
`beartype._util.hint.pep.utilhintpepget.get_hint_pep_sign()` getter to
transparently support both the `typing` and `typing_extensions` modules.
(*Distributional ebullience!*)
  • Loading branch information
leycec committed May 28, 2021
1 parent c15ff0e commit 783e699
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 55 deletions.
2 changes: 1 addition & 1 deletion beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
HINT_PEP484_SIGNS_UNION,
)
from beartype._util.hint.data.utilhintdata import HINTS_IGNORABLE_SHALLOW
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP586_SIGN_LITERAL,
HINT_PEP593_SIGN_ANNOTATED,
)
Expand Down
2 changes: 1 addition & 1 deletion beartype/_decor/_error/_proposal/_errorpep586.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# ....................{ IMPORTS }....................
from beartype._decor._error._errorsleuth import CauseSleuth
from beartype._decor._error._errortype import get_cause_or_none_type_origin
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP586_SIGN_LITERAL)
from beartype._util.text.utiltextjoin import join_delimited_disjunction
from beartype._util.text.utiltextrepr import represent_object
Expand Down
2 changes: 1 addition & 1 deletion beartype/_decor/_error/_proposal/_errorpep593.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# ....................{ IMPORTS }....................
from beartype.roar._roarexc import _BeartypeCallHintPepRaiseException
from beartype._decor._error._errorsleuth import CauseSleuth
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP593_SIGN_ANNOTATED)
from beartype._util.hint.pep.proposal.utilhintpep593 import (
get_hint_pep593_metadata,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ def _init() -> None:

# ..................{ IMPORTS }..................
# Defer Python version-specific imports.
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP_SIGN_LIST)
from typing import (
Any,
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/hint/pep/proposal/utilhintpep586.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintPep586Exception
from beartype._cave._cavefast import EnumMemberType, NoneType
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP586_SIGN_LITERAL)
from beartype._util.text.utiltextjoin import join_delimited_disjunction_classes
from typing import Any
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/hint/pep/proposal/utilhintpep593.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintPep593Exception
from beartype._vale._valesub import _SubscriptedIs
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP593_SIGN_ANNOTATED)
from typing import Any, Optional, Tuple

Expand Down
94 changes: 46 additions & 48 deletions beartype/_util/hint/pep/utilhintpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
)
from beartype._util.hint.data.pep.proposal.utilhintdatapep484 import (
HINT_PEP484_TYPE_FORWARDREF)
from beartype._util.lib.utilliboptional import (
IS_LIB_TYPING_EXTENSIONS)
from beartype._util.py.utilpyversion import (
IS_PYTHON_3_6,
IS_PYTHON_AT_LEAST_3_7,
Expand All @@ -39,6 +41,7 @@
is_hint_pep585_builtin,
is_hint_pep585_generic,
)
from beartype._util.py.utilpymodule import import_module
from typing import Any, Generic, NewType, Optional, Tuple, TypeVar

# See the "beartype.cave" submodule for further commentary.
Expand Down Expand Up @@ -551,59 +554,47 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# Ergo, this string is effectively the *ONLY* sane means of deciding which
# broad category of behaviour an arbitrary PEP 484-compliant type hint
# conforms to.
sign_name = repr(hint)
hint_name = repr(hint)

# If this representation is prefixed by neither...
# Split this representation on the first "." into:
# * "hint_module_name", the unqualified name of the module declaring this
# hint.
# * "sign_name", the unqualified name of the possibly subscripted attribute
# of this module defining this hint.
hint_module_name, _, hint_module_attr_name = hint_name.partition('.')

# If neither...
if not (
# "typing" (i.e., Python's official typing module in the sdlib)
# *NOR*...
sign_name.startswith('typing.')

#FIXME: Uncomment these two tests *AFTER* we've defined a new
#"beartype._util.lib.utilliboptional.IS_LIB_TYPING_EXTENSIONS" global
#boolean defined as follows:
# from beartype._util.py.utilpymodule import is_module
#
# IS_LIB_TYPING_EXTENSIONS = is_module('typing_extensions)

# sign_name.startswith('typing.') or
# # "typing_extensions" (i.e., a third-party quasi-official typing module
# # backporting "typing" hints introduced in newer Python versions to
# # older Python versions)...
# (
# IS_LIB_TYPING_EXTENSIONS and
# sign_name.startswith('typing_extensions.')
# )
# This module is "typing" (i.e., the official typing module bundled
# with the Python stdlib) *NOR*...
hint_module_name == 'typing' or (
# The third-party "typing_extensions" module (i.e., a third-party
# quasi-official typing module backporting "typing" hints
# introduced in newer Python versions to older Python versions) is
# importable under the active Python interpreter *AND*...
IS_LIB_TYPING_EXTENSIONS and
# This module is "typing_extensions"...
hint_module_name == 'typing_extensions.'
)
):
# Then this hint originates from neither the "typing" nor
# "typing_extensions" modules and is thus *NOT* PEP-compliant. But by the
# validation above, this hint is PEP-compliant. Since this invokes a
# world-shattering paradox, raise an exception.
raise BeartypeDecorHintPepSignException(
f'Type hint {repr(hint)} representation "{sign_name}" not '
f'Type hint {repr(hint)} representation "{hint_name}" not '
f'prefixed by "typing." or "typing_extensions.".'
)

# Strip the now-harmful "typing." prefix from this representation.
# Preserving this prefix would raise an "AttributeError" exception from
# the subsequent call to the getattr() builtin.
sign_name = sign_name[7:] # hardcode us up the bomb
#FIXME: Probably faster to just call:
# sign_name, _, _ = sign_name.partition('[')

# 0-based index of the first "[" delimiter in this representation if
# any *OR* -1 otherwise.
sign_name_bracket_index = sign_name.find('[')

# If this representation contains such a delimiter, this is a subscripted
# type hint. In this case, reduce this representation to its unsubscripted
# form by truncating the suffixing parametrization from this representation
# (e.g., from "typing.Union[str, typing.Sequence[int]]" to merely
# "typing.Union").
#
# Note that this is the common case and thus explicitly tested first.
if sign_name_bracket_index > 0:
sign_name = sign_name[:sign_name_bracket_index]
# Else, this representation contains no such delimiter and is thus already
# unsubscripted. In this case, preserve this representation as is.
# Unqualified name of this unsubscripted attribute. Specifically, if the
# unqualified name of this possibly subscripted attribute was:
# * Already unsubscripted (e.g., "List"), this is the same name unmodified.
# * Subscripted (e.g., "List[str]"), this is the prefix of that name
# preceding the first "[" delimiter in that name.
sign_name, _, _ = hint_module_attr_name.partition('[')

# If this name erroneously refers to a non-existing "typing" attribute,
# rewrite this name to refer to the actual existing "typing" attribute
Expand All @@ -612,18 +603,25 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# "typing.ContextManager" attribute).
sign_name = _HINT_PEP_TYPING_NAME_BAD_TO_GOOD.get(sign_name, sign_name)

# "typing" attribute with this name if any *OR* "None" otherwise.
sign = getattr(typing, sign_name, None)
# Module declaring this unsubscripted attribute if importable *OR* raise
# an exception otherwise (i.e., if this module is unimportable).
hint_module = import_module(
module_name=hint_module_name,
exception_cls=BeartypeDecorHintPepSignException,
)

# Unsubscripted attribute with this name declared by this module if any
# *OR* "None" otherwise.
sign = getattr(hint_module, sign_name, None)

# If this "typing" attribute does *NOT* exist...
# If this module declares *NO* such attribute, raise an exception.
if sign is None:
raise BeartypeDecorHintPepSignException(
f'PEP 484 type hint {repr(hint)} '
f'attribute "typing.{sign_name}" not found.'
f'Type hint {repr(hint)} attribute "typing.{sign_name}" not found.'
)
# Else, this "typing" attribute exists.
# Else, this module declares this attribute.

# Return this "typing" attribute.
# Return this attribute.
return sign

# ....................{ GETTERS ~ type : generic }....................
Expand Down
Empty file added beartype/_util/lib/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions beartype/_util/lib/utilliboptional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# Copyright (c) 2014-2021 Beartype authors.
# See "LICENSE" for further details.

'''
Project-wide **optional runtime dependency** (i.e., third-party Python packages
optionally imported where importable by :mod:`beartype`) utilities.
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ IMPORTS }....................
from beartype._util.py.utilpymodule import is_module

# ....................{ CONSTANTS }....................
IS_LIB_TYPING_EXTENSIONS = is_module('typing_extensions')
'''
``True`` only if the third-party :mod:`typing_extensions` module is importable
under the active Python interpreter.
:mod:`typing_extensions` backports attributes of the :mod:`typing` module
bundled with newer Python versions to older Python versions.
'''
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def test_raise_pep_call_exception() -> None:
from beartype.roar._roarexc import _BeartypeCallHintPepRaiseException
from beartype._decor._error.errormain import (
raise_pep_call_exception)
from beartype._util.hint.data.pep.utilhintdatapepsign import (
from beartype._util.hint.data.pep.utilhintdatapepattr import (
HINT_PEP_SIGN_LIST,
HINT_PEP_SIGN_TUPLE,
)
Expand Down

0 comments on commit 783e699

Please sign in to comment.