Skip to content

Commit

Permalink
"typing_extensions.Annotated" x 1.
Browse files Browse the repository at this point in the history
This commit is the first 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 both implements and tests the new private
`beartype._util.py.utilpymodule.is_module()` tester required to
efficiently and safely detect optional third-party dependencies like
`typing_extensions` as well as resolving a trivial documentation
code-string error kindly noted by machine learning guru and all-around
stand-up paragon of GitHub virtue @rsokl. (*Felonious melons!*)
  • Loading branch information
leycec committed May 27, 2021
1 parent 8fefc15 commit c15ff0e
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 71 deletions.
16 changes: 8 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -591,18 +591,18 @@ non-trivial combinations of nested type hints compliant with different PEPs:
from beartype import beartype
from collections.abc import Sequence
from numpy import dtype, empty_like, ndarray
from typing import Optional, Union
import numpy as np
@beartype
def empty_like_bear(
prototype: object,
dtype: Optional[dtype] = None,
dtype: Optional[np.dtype] = None,
order: str = 'K',
subok: bool = True,
shape: Optional[Union[int, Sequence[int]]] = None,
) -> ndarray:
return empty_like(prototype, dtype, order, subok, shape)
) -> np.ndarray:
return np.empty_like(prototype, dtype, order, subok, shape)
Note the non-trivial hint for the optional ``shape`` parameter, synthesized
from a `PEP 484-compliant optional <typing.Optional_>`__ of a `PEP
Expand Down Expand Up @@ -692,7 +692,7 @@ hints with compact two-line **beartype validators:**
# Type hint matching any two-dimensional NumPy array of floats of arbitrary
# precision. Yup. That's a beartype validator, folks!
Numpy2DFloatArray = Annotated[ndarray, Is[lambda array:
Numpy2DFloatArray = Annotated[np.ndarray, Is[lambda array:
array.ndim == 2 and np.issubdtype(array.dtype, np.floating)]]
# Annotate @beartype-decorated callables with beartype validators.
Expand Down Expand Up @@ -1059,7 +1059,7 @@ the functional validator in that example:
# Type hint matching only two-dimensional NumPy arrays of floats of
# arbitrary precision. This time, do it faster than anyone has ever
# type-checked NumPy arrays before. (Cue sonic boom, Chuck Yeager.)
Numpy2DFloatArray = Annotated[ndarray,
Numpy2DFloatArray = Annotated[np.ndarray,
IsAttr['ndim', IsEqual[2]] &
IsAttr['dtype',
IsAttr['type', IsEqual[np.float32] | IsEqual[np.float64]]]
Expand Down Expand Up @@ -1153,7 +1153,7 @@ validators mean you no longer have to accept the QA scraps we feed you:
from typing import Annotated
# Type hint matching all integers in a list of integers in O(n) time. Please
# never do this. You want to now, don't you? Why? You know the price! Why?!?
# never do this. You now want to, don't you? Why? You know the price! Why?!?
IntList = Annotated[list[int], Is[lambda lst: all(
isinstance(item, int) for item in lst)]]
Expand Down Expand Up @@ -1387,7 +1387,7 @@ Let's type-check like `greased lightning`_:
def __new__(cls, *args: str) -> Tuple[str, ...]:
return tuple.__new__(cls, args)
# ..................{ VALIDATORS }..................
# ..................{ VALIDATORS }..................
# Import PEP 593-compliant beartype-specific type hints validating arbitrary
# caller constraints. Note this requires Python ≥ 3.9 and beartype ≥ 0.7.0.
from beartype.vale import Is, IsAttr, IsEqual
Expand Down
35 changes: 10 additions & 25 deletions beartype/_util/hint/data/pep/proposal/utilhintdatapep585.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# See "LICENSE" for further details.

'''
**Beartype `PEP 585`_**-compliant type hint data.**
**Beartype :pep:`585`**-compliant type hint data.**
This private submodule is *not* intended for importation by downstream callers.
Expand All @@ -24,61 +24,46 @@
HINT_PEP585_TUPLE_EMPTY = (
tuple[()] if IS_PYTHON_AT_LEAST_3_9 else Iota()) # type: ignore[misc]
'''
`PEP 585`_-compliant empty fixed-length tuple type hint if the active Python
interpreter supports at least Python 3.9 and thus `PEP 585`_ *or* a unique
:pep:`585`-compliant empty fixed-length tuple type hint if the active Python
interpreter supports at least Python 3.9 and thus :pep:`585` *or* a unique
placeholder object otherwise to guarantee failure when comparing arbitrary
objects against this object via equality tests.
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# ....................{ SETS ~ sign }....................
HINT_PEP585_SIGNS_SUPPORTED_DEEP: FrozenSet[Any] = frozenset()
'''
Frozen set of all `PEP 585`_-compliant **deeply supported signs** (i.e.,
arbitrary objects uniquely identifying `PEP 585`_-compliant type hints for
Frozen set of all :pep:`585`-compliant **deeply supported signs** (i.e.,
arbitrary objects uniquely identifying :pep:`585`-compliant type hints for
which the :func:`beartype.beartype` decorator generates deep type-checking
code).
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''


HINT_PEP585_SIGNS_TYPE: FrozenSet[Any] = frozenset()
'''
Frozen set of all `PEP 585`_-compliant **standard class signs** (i.e.,
Frozen set of all :pep:`585`-compliant **standard class signs** (i.e.,
instances of the builtin :mod:`type` type uniquely identifying PEP-compliant
type hints).
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# ....................{ SETS ~ sign : category }....................
HINT_PEP585_SIGNS_SEQUENCE_STANDARD: FrozenSet[Any] = frozenset()
'''
Frozen set of all `PEP 585`_-compliant **standard sequence signs** (i.e.,
arbitrary objects uniquely identifying `PEP 585`_-compliant type hints
Frozen set of all :pep:`585`-compliant **standard sequence signs** (i.e.,
arbitrary objects uniquely identifying :pep:`585`-compliant type hints
accepting exactly one subscripted type hint argument constraining *all* items
of compliant sequences, which necessarily satisfy the
:class:`collections.abc.Sequence` protocol with guaranteed ``O(1)`` indexation
across all sequence items).
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''


HINT_PEP585_SIGNS_TUPLE: FrozenSet[Any] = frozenset()
'''
Frozen set of all `PEP 585`_-compliant **tuple signs** (i.e., arbitrary objects
uniquely identifying `PEP 585`_-compliant type hints accepting exactly one
Frozen set of all :pep:`585`-compliant **tuple signs** (i.e., arbitrary objects
uniquely identifying :pep:`585`-compliant type hints accepting exactly one
subscripted type hint argument constraining *all* items of compliant tuples).
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# ....................{ INITIALIZERS }....................
Expand Down
83 changes: 47 additions & 36 deletions beartype/_util/hint/pep/utilhintpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,20 +304,21 @@ def get_hint_pep_sign(hint: Any) -> object:
Specifically, this function returns either:
* If this hint is a `PEP 585`_-compliant **builtin** (e.g., C-based type
* If this hint is a :pep:`585`-compliant **builtin** (e.g., C-based type
hint instantiated by subscripting either a concrete builtin container
class like :class:`list` or :class:`tuple` *or* an abstract base class
(ABC) declared by the :mod:`collections.abc` submodule like
:class:`collections.abc.Iterable` or :class:`collections.abc.Sequence`),
:class:`beartype.cave.HintGenericSubscriptedType`.
* If this hint is a **generic** (i.e., subclasses of the
:class:`typing.Generic` abstract base class (ABC)),
:class:`typing.Generic`. Note this includes `PEP 544`-compliant
:class:`typing.Generic`. Note this includes :pep:`544`-compliant
**protocols** (i.e., subclasses of the :class:`typing.Protocol` ABC),
which implicitly subclass the :class:`typing.Generic` ABC as well.
* If this hint is any other class declared by either the :mod:`typing`
module (e.g., :class:`typing.TypeVar`) *or* the :mod:`beartype.cave`
submodule (e.g., :class:`beartype.cave.HintGenericSubscriptedType`), that class.
submodule (e.g., :class:`beartype.cave.HintGenericSubscriptedType`), that
class.
* If this hint is a **forward reference** (i.e., string or instance of the
concrete :class:`typing.ForwardRef` class), :class:`typing.ForwardRef`.
* If this hint is a **type variable** (i.e., instance of the concrete
Expand All @@ -330,7 +331,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
Motivation
----------
Both `PEP 484`_ and the :mod:`typing` module implementing `PEP 484`_ are
Both :pep:`484` and the :mod:`typing` module implementing :pep:`484` are
functionally deficient with respect to their public APIs. Neither provide
external callers any means of deciding the categories of arbitrary
PEP-compliant type hints. For example, there exists no general-purpose
Expand Down Expand Up @@ -381,11 +382,6 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
>>> class Duplicity(typing.Iterable[T], typing.Container[T]): pass
>>> get_hint_pep_sign(Duplicity)
typing.Iterable
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# Avoid circular import dependencies.
Expand Down Expand Up @@ -557,14 +553,35 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# conforms to.
sign_name = repr(hint)

# If this representation is *NOT* prefixed by "typing,", this hint does
# *NOT* originate from the "typing" module 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
if not sign_name.startswith('typing.'):
# If this representation is prefixed by 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.')
# )
):
# 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'PEP 484 type hint {repr(hint)} '
f'representation "{sign_name}" not prefixed by "typing.".'
f'Type hint {repr(hint)} representation "{sign_name}" not '
f'prefixed by "typing." or "typing_extensions.".'
)

# Strip the now-harmful "typing." prefix from this representation.
Expand Down Expand Up @@ -776,7 +793,7 @@ def get_hint_pep_generic_bases_unerased(hint: object) -> Tuple[object, ...]:
'''
Tuple of all **unerased pseudo-superclasses** (i.e., PEP-compliant objects
originally listed as superclasses prior to their implicit type erasure
under `PEP 560`_) of the passed PEP-compliant **generic** (i.e., class
under :pep:`560`) of the passed PEP-compliant **generic** (i.e., class
superficially subclassing at least one non-class PEP-compliant object) if
this object is a generic *or* raise an exception otherwise (i.e., if this
object is either not a class *or* is a class subclassing no non-class
Expand Down Expand Up @@ -806,32 +823,32 @@ def get_hint_pep_generic_bases_unerased(hint: object) -> Tuple[object, ...]:
Motivation
----------
`PEP 560`_ (i.e., "Core support for typing module and generic types)
:pep:`560` (i.e., "Core support for typing module and generic types)
formalizes the ``__orig_bases__`` dunder attribute first informally
introduced by the :mod:`typing` module's implementation of `PEP 484`_.
Naturally, `PEP 560`_ remains as unusable as `PEP 484`_ itself. Ideally,
`PEP 560`_ would have generalized the core intention of preserving each
introduced by the :mod:`typing` module's implementation of :pep:`484`.
Naturally, :pep:`560` remains as unusable as :pep:`484` itself. Ideally,
:pep:`560` would have generalized the core intention of preserving each
original user-specified subclass tuple of superclasses as a full-blown
``__orig_mro__`` dunder attribute listing the original method resolution
order (MRO) of that subclass had that tuple *not* been modified.
Naturally, `PEP 560`_ did no such thing. The original MRO remains
Naturally, :pep:`560` did no such thing. The original MRO remains
obfuscated and effectively inaccessible. While computing that MRO would
technically be feasible, doing so would also be highly non-trivial,
expensive, and fragile. Instead, this function retrieves *only* the tuple
of :mod:`typing`-specific pseudo-superclasses that this object's class
originally attempted (but failed) to subclass.
You are probably now agitatedly cogitating to yourself in the darkness:
"But @leycec: what do you mean `PEP 560`_? Wasn't `PEP 560`_ released
*after* `PEP 484`_? Surely no public API defined by the Python stdlib would
"But @leycec: what do you mean :pep:`560`? Wasn't :pep:`560` released
*after* :pep:`484`? Surely no public API defined by the Python stdlib would
be so malicious as to silently alter the tuple of base classes listed by a
user-defined subclass?"
As we've established both above and elsewhere throughout the codebase,
everything developed for `PEP 484` -- including `PEP 560`_, which derives
its entire raison d'etre from `PEP 484`_ -- are fundamentally insane. In
this case, `PEP 484`_ is insane by subjecting parametrized :mod:`typing`
everything developed for `PEP 484` -- including :pep:`560`, which derives
its entire raison d'etre from :pep:`484` -- are fundamentally insane. In
this case, :pep:`484` is insane by subjecting parametrized :mod:`typing`
types employed as base classes to "type erasure," because:
...it is common practice in languages with generics (e.g. Java,
Expand Down Expand Up @@ -883,7 +900,7 @@ def get_hint_pep_generic_bases_unerased(hint: object) -> Tuple[object, ...]:
the generally useless ``__mro__`` dunder tuple. Note, however, that the
latter *is* still occasionally useful and thus occasionally returned by
this getter. For inexplicable reasons, **single-inherited protocols**
(i.e., classes directly subclassing *only* the `PEP 544`_-compliant
(i.e., classes directly subclassing *only* the :pep:`544`-compliant
:attr:`typing.Protocol` abstract base class (ABC)) are *not* subject to
type erasure and thus constitute a notable exception to this heuristic:
Expand All @@ -894,7 +911,8 @@ def get_hint_pep_generic_bases_unerased(hint: object) -> Tuple[object, ...]:
>>> UserDefinedProtocol.__mro__
(__main__.UserDefinedProtocol, typing.Protocol, typing.Generic, object)
>>> UserDefinedProtocol.__orig_bases__
AttributeError: type object 'UserDefinedProtocol' has no attribute '__orig_bases__'
AttributeError: type object 'UserDefinedProtocol' has no attribute
'__orig_bases__'
Welcome to :mod:`typing` hell, where even :mod:`typing` types lie broken
and misshapen on the killing floor of overzealous theory-crafting purists.
Expand Down Expand Up @@ -936,13 +954,6 @@ def get_hint_pep_generic_bases_unerased(hint: object) -> Tuple[object, ...]:
collections.abc.Container,
typing.Generic,
object)
.. _PEP 484:
https://www.python.org/dev/peps/pep-0484
.. _PEP 560:
https://www.python.org/dev/peps/pep-0560
.. _PEP 585:
https://www.python.org/dev/peps/pep-0585
'''

# Tuple of either...
Expand Down
50 changes: 50 additions & 0 deletions beartype/_util/py/utilpymodule.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# ....................{ IMPORTS }....................
import importlib
from beartype.roar._roarexc import _BeartypeUtilModuleException
from sys import modules as sys_modules
from types import ModuleType
from typing import Any, Optional, Type

Expand Down Expand Up @@ -103,6 +104,55 @@ def die_unless_module_attr_name(
# Else, this string is syntactically valid as a fully-qualified module
# attribute name.

# ....................{ TESTERS }....................
def is_module(module_name: str) -> bool:
'''
``True`` only if the module, package, or C extension with the passed
fully-qualified name is importable under the active Python interpreter.
Caveats
----------
**This tester dynamically imports this module as an unavoidable side effect
of performing this test.**
Parameters
----------
module_name : str
Fully-qualified name of the module to be imported.
Returns
----------
bool
``True`` only if this module is importable.
Raises
----------
Exception
If a module with this name exists *but* this module is unimportable
due to module-scoped side effects at importation time. Since modules
may perform arbitrary Turing-complete logic from module scope, callers
should be prepared to handle *any* possible exception that might arise.
'''
assert isinstance(module_name, str), f'{repr(module_name)} not string.'

# If this module has already been imported, return true.
if module_name in sys_modules:
return True
# Else, this module has yet to be imported.

# Attempt to...
try:
# Dynamically import this module.
importlib.import_module(module_name)

# Return true, since this importation succeeded.
return True
# If no module this this name exists, return false.
except ModuleNotFoundError:
return False
# If any other exception was raised, silently permit that exception to
# unwind the call stack.

# ....................{ GETTERS ~ object : name }....................
def get_object_module_name(obj: object) -> str:
'''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ def test_get_hint_pep_sign_pass() -> None:

# Defer heavyweight imports.
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_sign
from beartype_test.a00_unit.data.hint.pep.data_hintpep import HINTS_PEP_META
from beartype_test.a00_unit.data.hint.pep.data_hintpep import (
HINTS_PEP_META)

# Assert this getter returns the expected unsubscripted "typing" attribute
# for all PEP-compliant type hints associated with such an attribute.
Expand Down

0 comments on commit c15ff0e

Please sign in to comment.