Skip to content

Commit

Permalink
"typing_extensions.Annotated" x 23.
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! (*Accidental dentistry!*)
  • Loading branch information
leycec committed Jul 9, 2021
1 parent e6c3843 commit db39962
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 128 deletions.
4 changes: 2 additions & 2 deletions beartype/_util/cls/utilclstest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
_BeartypeUtilTypeException,
)
from beartype._util.data.cls.datacls import TYPES_BUILTIN_FAKE
from beartype._util.data.mod.datamod import MODULE_NAME_BUILTINS
from beartype._util.data.mod.datamod import BUILTINS_MODULE_NAME
from typing import Type

# ....................{ VALIDATORS }....................
Expand Down Expand Up @@ -240,7 +240,7 @@ def is_type_builtin(cls: type) -> bool:

# This return true only if this name is that of the "builtins" module
# declaring all builtin types.
return cls_module_name == MODULE_NAME_BUILTINS
return cls_module_name == BUILTINS_MODULE_NAME

# ....................{ TESTERS ~ isinstanceable }....................
def is_type_isinstanceable(cls: object) -> bool:
Expand Down
24 changes: 0 additions & 24 deletions beartype/_util/data/hint/pep/datapepmodule.py

This file was deleted.

25 changes: 18 additions & 7 deletions beartype/_util/data/hint/pep/datapeprepr.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,7 @@ def _init() -> None:

# ..................{ EXTERNALS }..................
# Defer initialization-specific imports.
from beartype._util.data.hint.pep.datapepmodule import (
HINT_PEP_MODULE_NAMES)
from beartype._util.data.mod.datamod import TYPING_MODULE_NAMES

# Permit redefinition of these globals below.
global \
Expand Down Expand Up @@ -372,11 +371,23 @@ def _init() -> None:
'Union',
}

# If the active Python interpreter targets Python 3.6, shallowly ignore the
# unsubscripted "Generic" superclass whose idiosyncratic representation
# under Python 3.6 is "typing.Generic" rather than "<class 'Generic'>"".
# Note that logic above already handles the latter case.
# If the active Python interpreter targets Python 3.6...
#
# Gods... these are horrible. Thanks for nuthin', Python 3.6.
if IS_PYTHON_3_6:
# Map from the idiosyncratic machine-readable bare representations of
# the "typing.Match" and "typing.Pattern" objects, which unlike all
# other "typing"-based type hints are *NOT* prefixed by "typing"
# (e.g., "Pattern[~AnyStr]" rather than "typing.Pattern").
HINT_BARE_REPR_TO_SIGN.update({
'Match': HintSignMatch,
'Pattern': HintSignPattern,
})

# Shallowly ignore the unsubscripted "Generic" superclass whose
# idiosyncratic representation under Python 3.6 is "typing.Generic"
# rather than "<class 'Generic'>"". Note that logic above already
# handles the latter case.
_HINT_TYPING_ATTR_NAMES_IGNORABLE.add('Generic')

# ..................{ HINTS ~ types }..................
Expand Down Expand Up @@ -404,7 +415,7 @@ def _init() -> None:

# ..................{ CONSTRUCTION }..................
# For the name of each top-level hinting module...
for typing_module_name in HINT_PEP_MODULE_NAMES:
for typing_module_name in TYPING_MODULE_NAMES:
# For each deprecated PEP 484-compliant typing attribute name...
for typing_attr_name in _HINT_PEP484_TYPING_ATTR_NAMES_DEPRECATED:
# Add that attribute relative to this module to this set.
Expand Down
24 changes: 19 additions & 5 deletions beartype/_util/data/mod/datamod.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,39 @@
__all__ = ['STAR_IMPORTS_CONSIDERED_HARMFUL']

# ....................{ NAMES }....................
MODULE_NAME_BUILTINS = 'builtins'
BUILTINS_MODULE_NAME = 'builtins'
'''
Fully-qualified name of the **builtins module** (i.e., objects defined by the
standard :mod:`builtins` module and thus globally available by default
*without* requiring explicit importation).
'''

# ....................{ SETS }....................
#FIXME: Replace all usage of "HINT_PEP_MODULE_NAMES" with usage of this set;
#then excise the former, please.
MODULE_NAMES_HINT = frozenset((
TYPING_MODULE_NAMES = frozenset((
# Name of the official typing module bundled with the Python stdlib.
'typing',
# Name of the third-party "typing_extensions" module, backporting "typing"
# hints introduced in newer Python versions to older Python versions.
'typing_extensions',
))
'''
Frozen set of the fully-qualified names of all **hinting modules** (i.e.,
Frozen set of the fully-qualified names of all **typing modules** (i.e.,
modules officially declaring attributes usable for creating PEP-compliant type
hints accepted by both static and runtime type checkers).
'''


TYPING_MODULE_NAMES_DOTTED = frozenset(
f'{typing_module_name}.' for typing_module_name in TYPING_MODULE_NAMES)
'''
Frozen set of the fully-qualified ``.``-suffixed names of all typing modules.
This set is a negligible optimization enabling callers to perform slightly more
efficient testing of string prefixes against items of this specialized set than
those of the more general-purpose :data:`TYPING_MODULE_NAMES` set.
See Also
----------
:data:`TYPING_MODULE_NAMES`
Further details.
'''
20 changes: 12 additions & 8 deletions beartype/_util/hint/pep/proposal/utilhintpep593.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintPep593Exception
from beartype._vale._valesub import _SubscriptedIs
from beartype._util.data.hint.pep.sign.datapepsigncls import HintSign
from beartype._util.data.hint.pep.sign.datapepsigns import HintSignAnnotated
from beartype._vale._valesub import _SubscriptedIs
from typing import Any, Optional, Tuple

# See the "beartype.cave" submodule for further commentary.
Expand Down Expand Up @@ -44,16 +44,16 @@ def die_unless_hint_pep593(hint: object) -> None:
f'PEP 593 type hint {repr(hint)} not "typing.Annotated".')

# ....................{ TESTERS }....................
#FIXME: Note this returns false for the unsubscripted "Annotated" class. Do
#we particularly care about this edge case? Probably not. *shrug*
#FIXME: This tester is now trivially silly. Excise as follows:
#* Replace all calls to this tester with tests resembling:
# get_hint_pep_sign_or_none(hint) is HintSignAnnotated
#* Excise this tester.
def is_hint_pep593(hint: object) -> bool:
'''
``True`` only if the passed object is a :pep:`593`-compliant **type
metahint** (i.e., subscription of the :attr:`typing.Annotated` singleton).
This tester is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
to an efficient one-liner.
This tester is memoized for efficiency.
Parameters
----------
Expand All @@ -67,15 +67,19 @@ def is_hint_pep593(hint: object) -> bool:
type metahint.
'''

# Avoid circular import dependencies.
from beartype._util.hint.pep.utilhintpepget import (
get_hint_pep_sign_or_none)

# Return true only if the machine-readable representation of this object
# implies this object to be a PEP 593-compliant type hint hint.
#
# Note that this approach has been intentionally designed to apply to
# arbitrary and hence possibly PEP-noncompliant type hints. Notably, this
# approach avoids the following equally applicable but considerably less
# efficient heuristic:
# return is_hint_pep(hint) and get_hint_pep_sign(hint) is Annotated
return repr(hint).startswith('typing.Annotated[')
# return is_hint_pep(hint) and get_hint_pep_sign(hint) is HintSignAnnotated
return get_hint_pep_sign_or_none(hint) is HintSignAnnotated


def is_hint_pep593_ignorable_or_none(
Expand Down
102 changes: 63 additions & 39 deletions beartype/_util/hint/pep/utilhintpepget.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,57 @@ def get_hint_pep_typevars(hint: object) -> Tuple[type, ...]:
'''

# ....................{ GETTERS ~ sign }....................
def get_hint_pep_sign(hint: Any) -> HintSign:
'''
**Sign** (i.e., :class:`HintSign` instance) uniquely identifying the passed
PEP-compliant type hint if PEP-compliant *or* raise an exception otherwise
(i.e., if this hint is *not* PEP-compliant).
This getter is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the implementation trivially reduces
to an efficient one-liner.
Parameters
----------
hint : object
Type hint to be inspected.
Returns
----------
dict
Sign uniquely identifying this hint.
Raises
----------
BeartypeDecorHintPepException
If this hint is *not* PEP-compliant.
BeartypeDecorHintPepSignException
If this object is a PEP-compliant type hint *not* uniquely identifiable
by a sign.
See Also
----------
'''

# 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 hint_sign is None:
raise BeartypeDecorHintPepSignException(
f'Type hint {repr(hint)} currently unsupported by beartype. '
f'You suddenly feel encouraged to submit '
f'a feature request for this hint to our '
f'friendly issue tracker at:\n\t{URL_ISSUES}'
)
# Else, this hint is unrecognized.

# Return the sign uniquely identifying this hint.
return hint_sign


#FIXME: Refactor all functions throughout the codebase that accept or return
#signs to be annotated ideally by "HintSign" (or, if necessary, by
#"HintSignOrType"). These include:
Expand All @@ -301,21 +352,22 @@ def get_hint_pep_typevars(hint: object) -> Tuple[type, ...]:
#FIXME: Validate that the value of the "pep_sign" parameter passed to the
#PepHintMetadata.__init__() constructor satisfies "HintSignOrType".
#FIXME: Refactor as follows:
#* Shift the majority of this function's body into a new
# get_hint_pep_sign_or_none() getter.
#* Refactor the following functions to mostly defer to that new
# get_hint_pep_sign_or_none() function:
# * This function.
# * The is_hint_pep() function.
#* Remove all now-unused "beartype._util.hint.pep.*" testers. Thanks to this
# dramatically simpler approach, we no longer require the excessive glut of
# PEP-specific testers we previously required.
#* Merge the contents of all now-minimal
# "beartype._util.data.hint.pep.proposal.*" submodules into their parent
# "beartype._util.hint.pep.proposal.*" submodules. There's no longer any
# demonstrable benefit to separating the two, so please cease doing so.
@callable_cached
def get_hint_pep_sign(hint: Any) -> HintSign:
def get_hint_pep_sign_or_none(hint: Any) -> Optional[HintSign]:
'''
**Sign** (i.e., :class:`HintSign` instance) uniquely identifying the passed
PEP-compliant type hint if this hint is PEP-compliant *or* raise an
exception otherwise (i.e., if this hint is *not* PEP-compliant).
PEP-compliant type hint if PEP-compliant *or* ``None`` otherwise (i.e., if
this hint is *not* PEP-compliant).
This getter function associates the passed hint with a public attribute of
the :mod:`typing` module effectively acting as a superclass of this hint
Expand Down Expand Up @@ -367,7 +419,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
Parameters
----------
hint : object
Object to be inspected.
Type hint to be inspected.
Returns
----------
Expand All @@ -376,17 +428,8 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
Raises
----------
AttributeError
If this object's representation is prefixed by the substring
``"typing."``, but the remainder of that representation up to but *not*
including the first "[" character in that representation (e.g.,
``"Dict"`` for the object ``typing.Dict[str, Tuple[int, ...]]``) is
*not* an attribute of the :mod:`typing` module.
BeartypeDecorHintPepException
If this object is *not* a PEP-compliant type hint.
BeartypeDecorHintPepSignException
If this object is a PEP-compliant type hint *not* uniquely identifiable
by a sign.
If this hint is *not* PEP-compliant.
Examples
----------
Expand Down Expand Up @@ -507,7 +550,7 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# Ergo, the "typing.Generic" ABC uniquely identifies many but *NOT* all
# generics. While non-ideal, the failure of PEP 585-compliant generics to
# subclass a common superclass leaves us with little alternative.
if is_hint_pep_generic(hint):
elif is_hint_pep_generic(hint):
return HintSignGeneric
# Else, this hint is *NOT* a PEP 484- or 585-compliant generic.
#
Expand All @@ -522,29 +565,10 @@ class like :class:`list` or :class:`tuple` *or* an abstract base class
# '<function NewType.<locals>.new_type at 0x7fca39388050>'
elif is_hint_pep484_newtype(hint):
return HintSignNewType
#FIXME: Drop this like hot lead after dropping Python 3.6 support.
# If the active Python interpreter targets Python 3.6 *AND* this hint is a
# poorly designed Python 3.6-specific "type alias", this hint is a
# subscription of either the "typing.Match" or "typing.Pattern" objects. In
# this case, this hint declares a non-standard "name" instance variable
# whose value is either the literal string "Match" or "Pattern". Return the
# "typing" attribute with this name *OR* implicitly raise an
# "AttributeError" exception if something goes horribly awry.
#
# Gods... this is horrible. Thanks for nuthin', Python 3.6.
elif IS_PYTHON_3_6 and isinstance(hint, typing._TypeAlias): # type: ignore[attr-defined]
return getattr(typing, hint.name)

# ..................{ ERROR }..................
# Else, this hint is identifiable by *NO* sign. But (by the above
# validation) this hint is PEP-compliant and should thus be identifiable by
# some sign. Since this is paradoxically bad, raise an exception.
raise BeartypeDecorHintPepSignException(
f'Type hint {repr(hint)} currently unsupported by beartype. '
f'You suddenly feel encouraged to submit '
f'a feature request for this hint to our '
f'friendly issue tracker at:\n\t{URL_ISSUES}'
)
# Else, this hint is unrecognized. In this case, return "None".
return None

# ....................{ GETTERS ~ type : generic }....................
@callable_cached
Expand Down

0 comments on commit db39962

Please sign in to comment.