Skip to content

Commit

Permalink
"typing_extensions.Annotated" x 24.
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 continues disastrously breaking literally
everything by continuing to disembowel the feckless
`beartype._util.hint.data.pep.datapep` submodule and its untrustworthy
`beartype._util.hint.data.pep.proposal` crony subpackage in favour of
`beartype._util.hint.data.pep.sign`, which is the only subpackage left
standing. Save us from our reckless selves, GitHub! (*Trumpeting strumpets!*)
  • Loading branch information
leycec committed Jul 14, 2021
1 parent a2a0c21 commit a7b5799
Show file tree
Hide file tree
Showing 14 changed files with 224 additions and 142 deletions.
12 changes: 1 addition & 11 deletions beartype/_decor/_cache/cachehint.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,19 +173,9 @@ def coerce_hint_pep(
# Original instance of this hint *PRIOR* to being subsequently coerced.
hint_old = hint

# ..................{ PEP 484 }..................
# If this hint is the PEP 484-compliant "None" singleton, coerce this hint
# into the type of that singleton. This is explicitly required by PEP 484
# *and* implicitly required by Python convention, where the bodies of
# callables annotated as returning "None" are understood to contain *no*
# explicit "return" statements and thus implicitly return "None".
if hint is None:
hint = NoneType
# Else, this hint is *NOT* the PEP 484-compliant "None" singleton.
#
# ..................{ MYPY }..................
# If...
elif (
if (
# This hint annotates the return for the decorated callable *AND*...
pith_name == 'return' and
# The decorated callable is a binary dunder method (e.g., __eq__())...
Expand Down
34 changes: 24 additions & 10 deletions beartype/_decor/_code/_pep/_pephint.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@
from beartype._util.data.hint.pep.datapeprepr import (
HINT_REPRS_IGNORABLE_SHALLOW)
from beartype._util.data.hint.pep.sign.datapepsigns import (
HintSignAnnotated,
HintSignForwardRef,
HintSignGeneric,
HintSignLiteral,
HintSignTuple,
)
from beartype._util.data.hint.pep.sign.datapepsignset import (
Expand Down Expand Up @@ -734,6 +736,19 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
)

# ................{ REDUCTION }................
#FIXME: Inefficient. Calling the get_hint_pep_sign_or_none() getter
#once here to obtain the sign of this hint and then simply testing that
#sign is likely to be substantially faster and simpler than the current
#approach performed below. Make it so, please -- especially because
#doing so should enable us to remove most of these PEP-specific testers
#from the codebase.
#
#Note, of course, that we'll need to call that same getter a second
#time *IF AND ONLY IF* the current hint is actually reduced by this
#reduction logic. Yup; that's lookin' pretty darn efficient there.
#FIXME: Perform a similar refactoring to the
#beartype._decor._error._errorsleuth.CauseSleuth.__init__() method.

#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAVEATS: Synchronize changes here with the corresponding block of the
# beartype._decor._error._errorsleuth.CauseSleuth.__init__()
Expand Down Expand Up @@ -1146,15 +1161,18 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:

# For each subscripted argument of this union...
for hint_child in hint_childs:
#FIXME: Uncomment as desired for debugging. This test is
#currently a bit too costly to warrant uncommenting.
# Assert that this child hint is *NOT* shallowly ignorable.
# Why? Because any union containing one or more shallowly
# ignorable child hints is deeply ignorable and should thus
# have already been ignored after a call to the
# is_hint_ignorable() tester passed this union on handling
# the parent hint of this union.
assert repr(hint) not in HINT_REPRS_IGNORABLE_SHALLOW, (
f'{hint_curr_label} {repr(hint_curr)} child '
f'{repr(hint_child)} ignorable but not ignored.')
# assert (
# repr(hint_curr) not in HINT_REPRS_IGNORABLE_SHALLOW), (
# f'{hint_curr_label} {repr(hint_curr)} child '
# f'{repr(hint_child)} ignorable but not ignored.')

# If this child hint is PEP-compliant...
if is_hint_pep(hint_child):
Expand Down Expand Up @@ -1302,11 +1320,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
elif (
# Originates from an origin type and may thus be shallowly
# type-checked against that type *AND is either...
(
hint_curr_sign in HINT_SIGNS_TYPE_STDLIB or
#FIXME: Remove this shuddering horror after refactoring!
hint_curr_sign in HINT_SIGNS_TYPE_STDLIB
) and (
hint_curr_sign in HINT_SIGNS_TYPE_STDLIB and (
#FIXME: Ideally, this line should just resemble:
# not is_hint_pep_subscripted(hint_curr)
#Unfortunately, unsubscripted type hints under Python 3.6
Expand Down Expand Up @@ -1534,7 +1548,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# beartype-specific (i.e., metahint whose second argument is an
# instance of the "beartype._vale._valesub._SubscriptedIs" class
# produced by subscripting the "Is" class). In this case...
elif hint_curr_sign is HINT_PEP593_ATTR_ANNOTATED:
elif hint_curr_sign is HintSignAnnotated:
# PEP-compliant type hint annotated by this metahint, localized
# to the "hint_child" local variable to satisfy the public API
# of the _enqueue_hint_child() closure called below.
Expand Down Expand Up @@ -1857,7 +1871,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# objects that are instances of only *SIX* possible types, which is
# sufficiently limiting as to render this singleton patently absurd
# and a farce that we weep to even implement. In this case...
elif hint_curr_sign is HINT_PEP586_ATTR_LITERAL:
elif hint_curr_sign is HintSignLiteral:
# If this hint does *NOT* comply with PEP 586 despite being a
# "typing.Literal" subscription, raise an exception. *sigh*
die_unless_hint_pep586(hint_curr)
Expand Down
3 changes: 3 additions & 0 deletions beartype/_util/data/hint/pep/datapeprepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
HintSignMutableSet,
# HintSignNamedTuple,
# HintSignNewType,
HintSignNone,
# HintSignOptional,
HintSignOrderedDict,
# HintSignParamSpec,
Expand Down Expand Up @@ -89,6 +90,8 @@
# here are those *NOT* amenable to such inspection.
HINT_BARE_REPR_TO_SIGN: Dict[str, HintSign] = {
# ..................{ PEP 484 }..................
'None': HintSignNone,

#FIXME: This is a bit odd. If an unsubscripted "typing.Protocol" is
#ignorable, why wouldn't an unsubscripted "typing.Generic" be ignorable as
#well? Consider excising this, please.
Expand Down
45 changes: 30 additions & 15 deletions beartype/_util/data/hint/pep/sign/datapepsigns.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

from beartype._util.data.hint.pep.sign.datapepsigncls import HintSign

# ....................{ SIGNS }....................
# ....................{ SIGNS ~ explicit }....................
# Signs with explicit analogues in the stdlib "typing" module.
#
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: Signs defined by this module are synchronized with the "__all__"
# list global of the "typing" module bundled with the most recent CPython
Expand Down Expand Up @@ -49,19 +51,6 @@
# * Preserve attributes here that have since been removed from the "typing"
# module in that CPython release to ensure their continued usability when
# running beartype against older CPython releases.
#
# Lastly, note that:
# * "NoReturn" is contextually valid *ONLY* as a top-level return hint. Since
# this use case is extremely limited, we explicitly generate code for this
# use case outside of the general-purpose code generation pathway for
# standard type hints. Since "NoReturn" is an unsubscriptable singleton, we
# explicitly detect this type hint with an identity test and thus require
# *NO* sign to uniquely identify this type hint. Indeed, explicitly defining
# a sign uniquely identifying this type hint would erroneously encourage us
# to use that sign elsewhere. We should *NOT* do that, because "NoReturn" is
# invalid in almost all possible contexts. Of course, we actually previously
# did define a "NoReturn" sign and erroneously use that sign elsewhere, which
# is exactly why we do *NOT* do so now. In short, "NoReturn" is insane.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# Super-special typing primitives.
Expand Down Expand Up @@ -143,7 +132,24 @@
HintSignNewType = HintSign(name='NewType')
# no_type_check <-- unusable as a type hint
# no_type_check_decorator <-- unusable as a type hint
# NoReturn <-- generally unusable as a type hint (see above for commentary)

# Note that "NoReturn" is contextually valid *ONLY* as a top-level return hint.
# Since this use case is extremely limited, we explicitly generate code for
# this use case outside of the general-purpose code generation pathway for
# standard type hints. Since "NoReturn" is an unsubscriptable singleton, we
# explicitly detect this type hint with an identity test and thus require *NO*
# sign to uniquely identify this type hint.
#
# Theoretically, explicitly defining a sign uniquely identifying this type hint
# could erroneously encourage us to use that sign elsewhere, which we should
# avoid, because "NoReturn" is invalid in almost all possible contexts.
# Pragmatically, doing so nonetheless improves orthogonality when detecting and
# validating PEP-compliant type hints, which ultimately matters more than our
# subjective feels about the matter. Wisely, we choose the practical approach.
#
# In short, "NoReturn" is insane.
HintSignNoReturn = HintSign(name='NoReturn')

# overload <-- unusable as a type hint
HintSignParamSpecArgs = HintSign(name='ParamSpecArgs')
HintSignParamSpecKwargs = HintSign(name='ParamSpecKwargs')
Expand All @@ -160,6 +166,15 @@
HintSignMatch = HintSign(name='Match')
HintSignPattern = HintSign(name='Pattern')

# ....................{ SIGNS ~ implicit }....................
# Signs with *NO* explicit analogues in the stdlib "typing" module but
# nonetheless standardized by one or more PEPs.

# PEP 484 explicitly supports the "None" singleton, albeit implicitly:
# When used in a type hint, the expression None is considered equivalent to
# type(None).
HintSignNone = HintSign(name='None')

# ....................{ CLEANUP }....................
# Prevent all attributes imported above from polluting this namespace. Why?
# Logic elsewhere subsequently assumes a one-to-one mapping between the
Expand Down
22 changes: 9 additions & 13 deletions beartype/_util/data/hint/pep/sign/datapepsignset.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
'''

# ....................{ IMPORTS }....................
from beartype._cave._cavefast import NoneType
from beartype._util.data.hint.pep.sign.datapepsigns import (
HintSignAbstractSet,
HintSignAnnotated,
Expand Down Expand Up @@ -53,6 +52,7 @@
HintSignMutableSequence,
HintSignMutableSet,
HintSignNewType,
HintSignNone,
HintSignOptional,
HintSignOrderedDict,
HintSignPattern,
Expand Down Expand Up @@ -216,10 +216,12 @@
originating from a **standard origin type** (i.e., isinstanceable class such
that *all* objects satisfying this hint are instances of this class).
Any object is trivially type-checkable against an isinstanceable class by
passing that object and class to the :func:`isinstance` builtin. Ergo, *all*
objects annotated by hints identified by signs in this set are guaranteed to at
least be shallowly type-checkable from wrapper functions generated by the
All hints identified by signs in this set are guaranteed to define
``__origin__`` dunder instance variables whose values are the standard origin
types they originate from. Since any object is trivially type-checkable against
such a type by passing that object and type to the :func:`isinstance` builtin,
*all* objects annotated by hints identified by signs in this set are at least
shallowly type-checkable from wrapper functions generated by the
:func:`beartype.beartype` decorator.
'''

Expand All @@ -233,14 +235,6 @@
HintSignForwardRef,
HintSignNewType,
HintSignTypeVar,

#FIXME: Non-orthogonal. Types are *NOT* signs. This should be probably be
#explicitly tested for elsewhere, please.

# PEP 484 explicitly supports the "None" singleton: i.e.,
# When used in a type hint, the expression None is considered
# equivalent to type(None).
NoneType,
))
'''
Frozen set of all **shallowly supported non-originative signs** (i.e.,
Expand All @@ -252,6 +246,8 @@

HINT_SIGNS_SUPPORTED_DEEP = frozenset((
# ..................{ PEP 484 }..................
HintSignNone,

# Note that "typing.Union" implicitly subsumes "typing.Optional" *ONLY*
# under Python <= 3.9. The implementations of the "typing" module under
# those older Python versions transparently reduced "typing.Optional" to
Expand Down
34 changes: 21 additions & 13 deletions beartype/_util/hint/pep/utilhintpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,21 @@ def get_hint_pep_sign(hint: Any) -> HintSign:
# Sign uniquely identifying this hint if recognized *OR* "None" otherwise.
hint_sign = get_hint_pep_sign_or_none(hint)

# If this hint is unrecognized, raise an exception. By internal validation
# performed by the prior call, this hint is PEP-compliant and should thus
# have been recognized (and identifiable by some sign).
# If this hint is unrecognized...
if hint_sign is None:
# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpeptest import die_unless_hint_pep

# If this hint is *NOT* PEP-compliant, raise an exception.
die_unless_hint_pep(hint)
# Else, this hint is PEP-compliant. Since this hint is unrecognized,
# this hint *MUST* be currently unsupported by the @beartype decorator.

# Raise an exception indicating this.
#
# Note that we intentionally avoid calling the
# die_if_hint_pep_unsupported() function here, which calls the
# is_hint_pep_supported() function, which calls this function.
raise BeartypeDecorHintPepSignException(
f'Type hint {repr(hint)} currently unsupported by beartype. '
f'You suddenly feel encouraged to submit '
Expand Down Expand Up @@ -452,19 +463,16 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
'''

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpeptest import (
die_unless_hint_pep, is_hint_pep_generic)

# If this hint is *NOT* PEP-compliant, raise an exception.
#
# Note that we intentionally avoid calling the
# die_if_hint_pep_unsupported() function here, which calls the
# is_hint_pep_supported() function, which calls this function.
die_unless_hint_pep(hint)
# Else, this hint is PEP-compliant.
from beartype._util.hint.pep.utilhintpeptest import is_hint_pep_generic

# For efficiency, this tester identifies the sign of this type hint with
# multiple phases performed in ascending order of average time complexity.
#
# Note that we intentionally avoid validating this type hint to be
# PEP-compliant (e.g., by calling the die_unless_hint_pep() validator).
# Why? Because this getter is the lowest-level hint validation function
# underlying all higher-level hint validation functions! Calling the latter
# here would thus induce infinite recursion, which would be very bad.

# ..................{ PHASE ~ repr }..................
# This phase attempts to map from the unsubscripted machine-readable
Expand Down
7 changes: 6 additions & 1 deletion beartype/_util/hint/pep/utilhintpeptest.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ def die_unless_hint_pep(
f'{hint_label} {repr(hint)} not PEP type hint.')

# ....................{ EXCEPTIONS ~ supported }....................
#FIXME: Refactor all or most calls to this and the
#FIXME: *DANGER.* This and the die_if_hint_pep_sign_unsupported() function make
#beartype more fragile. Instead, refactor all or most calls to this and the
#die_if_hint_pep_sign_unsupported() functions into calls to the
#warn_if_hint_pep_unsupported() function; then, consider excising these as well
#as exception classes (e.g., "BeartypeDecorHintPepUnsupportedException").
Expand Down Expand Up @@ -747,6 +748,10 @@ def is_hint_pep_supported(hint: object) -> bool:
return is_hint_pep_sign_supported(hint_pep_sign)


#FIXME: Silly overkill, silly. Refactor as follows:
#* Replace all calls to this tester with simply:
# hint_sign in HINT_SIGNS_SUPPORTED
#* Remove this tester.
def is_hint_pep_sign_supported(hint_sign: HintSign) -> bool:
'''
``True`` only if the passed object is a **supported sign** (i.e., arbitrary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,20 @@
from pytest import raises

# ....................{ TESTS ~ sign }....................
def test_get_hint_pep_sign_pass() -> None:
def test_get_hint_pep_sign() -> None:
'''
Test successful usage of the
:func:`beartype._util.hint.pep.utilhintpepget.get_hint_pep_sign` getter.
'''

# Defer heavyweight imports.
from beartype.roar import (
BeartypeDecorHintPepException,
BeartypeDecorHintPepSignException,
)
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_sign
from beartype_test.a00_unit.data.hint.data_hint import (
NOT_HINTS_PEP, NonPepCustomFakeTyping)
from beartype_test.a00_unit.data.hint.pep.data_hintpep import (
HINTS_PEP_META)

Expand All @@ -36,22 +42,6 @@ def test_get_hint_pep_sign_pass() -> None:
assert get_hint_pep_sign(hint_pep_meta.hint) == (
hint_pep_meta.pep_sign)


def test_get_hint_pep_sign_fail() -> None:
'''
Test unsuccessful usage of the
:func:`beartype._util.hint.pep.utilhintpepget.get_hint_pep_sign` getter.
'''

# Defer heavyweight imports.
from beartype.roar import (
BeartypeDecorHintPepException,
BeartypeDecorHintPepSignException,
)
from beartype._util.hint.pep.utilhintpepget import get_hint_pep_sign
from beartype_test.a00_unit.data.hint.data_hint import (
NOT_HINTS_PEP, NonPepCustomFakeTyping)

# Assert this getter raises the expected exception for an instance of a
# class erroneously masquerading as a "typing" class.
with raises(BeartypeDecorHintPepSignException):
Expand Down

0 comments on commit a7b5799

Please sign in to comment.