Skip to content

Commit

Permalink
PEP 563 + typing.NamedTuple x 3.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain explicitly supporting
`typing.NamedTuple` subclasses under PEP 563 (i.e., `from __future__
import annotations`), en-route to resolving issue #318 kindly submitted
by the cosmically rare-earth GitHub element @kasium. For unknown
reasons, `typing.NamedTuple` subclasses encapsulate type hints
stringified by PEP 563 as `typing.ForwardRef(...)` objects -- which is
just all manner of strange. In response, this commit exhaustively unit
tests improves to our internal resolution of `typing.ForwardRef(...)`
objects that additionally consider the
`typing.ForwardRef.__forward_module__` dunder attribute first introduced
by Python ≥ 3.9. (*Indifferent diffidence is no decadent dissidence!*)
  • Loading branch information
leycec committed Jan 31, 2024
1 parent f47b363 commit fd23bd9
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 379 deletions.
54 changes: 17 additions & 37 deletions beartype/_check/code/_codescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
# comments in the "beartype.plug._plughintable" submodule pertaining to that
# protocol for further details on properly building out this stack.
# * When that algorithm visits a forward reference:
# * That algorithm calls the express_func_scope_type_forwardref() function
# * That algorithm calls the express_func_scope_type_ref() function
# generating type-checking code for that reference. Refactor that call to
# additionally pass that stack of parent hints to that function.
# * Refactor the express_func_scope_type_forwardref() function to:
# * Refactor the express_func_scope_type_ref() function to:
# * If the passed forward reference is relative, additionally return that
# stack in the returned 3-tuple
# "(forwardref_expr, forwardrefs_class_basename, forwardref_parent_hints)",
Expand All @@ -36,7 +36,7 @@
# basename of an existing attribute in a local or global scope of the
# currently decorated callable *AND* the value of that attribute is a
# parent type hint on the stack of parent type hints returned by the
# previously called express_func_scope_type_forwardref() function, then
# previously called express_func_scope_type_ref() function, then
# *THIS REFERENCE INDICATES A RECURSIVE TYPE HINT.* In this case:
# * Replace this forward reference with a new recursive type-checking
# "beartype._check.forward.fwdref.BeartypeForwardRef_{forwardref}"
Expand All @@ -48,8 +48,11 @@

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintNonpepException
from beartype.typing import (
Optional,
Tuple,
)
from beartype._cave._cavemap import NoneTypeOr
from beartype._data.hint.datahinttyping import LexicalScope
from beartype._check.forward.fwdtype import (
TYPISTRY_HINT_NAME_TUPLE_PREFIX,
bear_typistry,
Expand All @@ -60,47 +63,24 @@
CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_PREFIX,
CODE_HINT_REF_TYPE_BASENAME_PLACEHOLDER_SUFFIX,
)
from beartype._data.cls.datacls import TYPES_SET_OR_TUPLE
from beartype._data.hint.datahinttyping import (
LexicalScope,
Pep484585ForwardRef,
SetOrTupleTypes,
TypeOrTupleTypes,
)
from beartype._util.cls.pep.utilpep3119 import die_unless_type_isinstanceable
from beartype._util.cls.utilclstest import is_type_builtin
from beartype._util.func.utilfuncscope import add_func_scope_attr
from beartype._util.hint.nonpep.utilnonpeptest import (
die_unless_hint_nonpep_type)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585ref import (
Pep484585ForwardRef,
die_unless_hint_pep484585_ref,
get_hint_pep484585_ref_name,
)
from beartype._util.utilobject import get_object_type_basename
from beartype._data.hint.datahinttyping import (
TypeOrTupleTypes,
TupleTypes,
)
from collections.abc import Set
from typing import AbstractSet, Optional, Tuple, Union

# ....................{ PRIVATE }....................
_SET_OR_TUPLE = (Set, tuple)
'''
2-tuple containing the superclasses of all frozen sets and tuples.
Note that the :class:`Set` abstract base class (ABC) rather than the concrete
:class:`set` subclass is intentionally listed here, as the concrete
:class:`frozenset` subclass subclasses the former but *not* latter: e.g.,
.. code-block:: python
>>> from collections.abc import Set
>>> issubclass(frozenset, Set)
True
>>> issubclass(frozenset, set)
False
'''


_SetOrTupleOfTypes = Union[AbstractSet[type], TupleTypes]
'''
PEP-compliant type hint matching a set *or* tuple of zero or more classes.
'''

# ....................{ ADDERS ~ type }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -246,7 +226,7 @@ def add_func_scope_type(

def add_func_scope_types(
# Mandatory parameters.
types: _SetOrTupleOfTypes,
types: SetOrTupleTypes,
func_scope: LexicalScope,

# Optional parameters.
Expand Down Expand Up @@ -360,7 +340,7 @@ def add_func_scope_types(
assert is_unique.__class__ is bool, f'{repr(is_unique)} not bool.'

# If this object is neither a set nor tuple, raise an exception.
if not isinstance(types, _SET_OR_TUPLE):
if not isinstance(types, TYPES_SET_OR_TUPLE):
raise BeartypeDecorHintNonpepException(
f'{exception_prefix}{repr(types)} neither set nor tuple.')
# Else, this object is either a set or tuple.
Expand Down Expand Up @@ -420,7 +400,7 @@ def add_func_scope_types(
attr=types, func_scope=func_scope, exception_prefix=exception_prefix)

# ....................{ EXPRESSERS ~ type }....................
def express_func_scope_type_forwardref(
def express_func_scope_type_ref(
# Mandatory parameters.
forwardref: Pep484585ForwardRef,
forwardrefs_class_basename: Optional[set],
Expand Down
6 changes: 3 additions & 3 deletions beartype/_check/code/codemake.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
add_func_scope_type,
add_func_scope_types,
add_func_scope_type_or_types,
express_func_scope_type_forwardref,
express_func_scope_type_ref,
)
from beartype._check.code.codesnip import (
PEP_CODE_HINT_CHILD_PLACEHOLDER_PREFIX,
Expand Down Expand Up @@ -841,7 +841,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# set instantiated by this call; else, this assignment
# preserves this local set as is.
hint_curr_expr, hint_refs_type_basename = (
express_func_scope_type_forwardref(
express_func_scope_type_ref(
forwardref=hint_curr,
forwardrefs_class_basename=(
hint_refs_type_basename),
Expand Down Expand Up @@ -1739,7 +1739,7 @@ def _enqueue_hint_child(pith_child_expr: str) -> str:
# Render this forward reference accessible to the body
# of this wrapper function. See above for commentary.
hint_curr_expr, hint_refs_type_basename = (
express_func_scope_type_forwardref(
express_func_scope_type_ref(
forwardref=hint_child, # type: ignore[arg-type]
forwardrefs_class_basename=(
hint_refs_type_basename),
Expand Down
2 changes: 1 addition & 1 deletion beartype/_check/convert/convcoerce.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def coerce_func_hint_root(
#type hints encapsulated by "typing.ForwardRef" objects, which the
#"typing.NamedTuple" superclass imposes on subclasses under PEP 563.
#FIXME: Ah-ha! We need to fundamentally improve the existing
#express_func_scope_type_forwardref() function as follows...
#express_func_scope_type_ref() function as follows...
#*WAIT.* Probably, we want to generalize the lower-level
#get_hint_pep484585_ref_name() getter as follows:
#* If the active Python interpreter targets Python >= 3.9 *AND* the passed
Expand Down
54 changes: 50 additions & 4 deletions beartype/_data/cls/datacls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
'''

# ....................{ IMPORTS }....................
from beartype.typing import FrozenSet
from beartype.typing import (
ForwardRef,
FrozenSet,
)
from beartype._cave._cavefast import (
#FIXME: Export these types from "beartype.cave", please.
AsyncCoroutineCType,
Expand Down Expand Up @@ -42,6 +45,9 @@
ClassDef,
FunctionDef,
)
from collections.abc import (
Set as SetABC,
)
from pathlib import Path

# ....................{ AST }....................
Expand Down Expand Up @@ -73,6 +79,24 @@
decorated by the :func:`beartype.beartype` decorator).
'''

# ....................{ CONTAINER }....................
TYPES_SET_OR_TUPLE = (SetABC, tuple)
'''
2-tuple containing the superclasses of all frozen sets and tuples.
Note that the :class:`Set` abstract base class (ABC) rather than the concrete
:class:`set` subclass is intentionally listed here, as the concrete
:class:`frozenset` subclass subclasses the former but *not* latter: e.g.,
.. code-block:: python
>>> from collections.abc import Set
>>> issubclass(frozenset, Set)
True
>>> issubclass(frozenset, set)
False
'''

# ....................{ FAKE ~ builtin }....................
# Defined below by the _init() function.
TYPES_BUILTIN_FAKE: FrozenSet[type] = None # type: ignore[assignment]
Expand All @@ -99,7 +123,7 @@
We're *not* dying on that lonely hill. We obey the Iron Law of Guido.
See Also
----------
--------
:data:`beartype_test.a00_unit.data.TYPES_BUILTIN_FAKE`
Related test-time set. Whereas this runtime-specific set is efficiently
defined explicitly by listing all non-builtin builtin mimic types, that
Expand All @@ -115,7 +139,7 @@
values encapsulating ``void*`` pointers in C extensions).
Caveats
----------
-------
**Python currently publishes *no* official PyCapsule type.** A little-known
technique to access the ``PyCapsule`` type from pure-Python code *does* exist
but is sufficiently non-portable to render that technique inadvisable in the
Expand All @@ -124,7 +148,7 @@
known to be portable across Python versions and various platforms.
See Also
----------
--------
https://stackoverflow.com/a/62258339/2809027
StackOverflow answer strongly inspiring this global.
https://stackoverflow.com/a/60319462/2809027
Expand All @@ -145,6 +169,28 @@
defined fake ``__enter__()`` dunder methods that are now deprecated.
'''

# ....................{ PEP ~ (484|585) }....................
TYPES_PEP484585_REF = (str, ForwardRef)
'''
Tuple union of all :pep:`484`- or :pep:`585`-compliant **forward reference
types** (i.e., classes of all forward reference objects).
Specifically, this union contains:
* :class:`str`, the class of all :pep:`585`-compliant forward reference objects
implicitly preserved by all :pep:`585`-compliant type hint factories when
subscripted by a string.
* :class:`HINT_PEP484_FORWARDREF_TYPE`, the class of all :pep:`484`-compliant
forward reference objects implicitly created by all :mod:`typing` type hint
factories when subscripted by a string.
While :pep:`585`-compliant type hint factories preserve string-based forward
references as is, :mod:`typing` type hint factories coerce string-based forward
references into higher-level objects encapsulating those strings. The latter
approach is the demonstrably wrong approach, because encapsulating strings only
harms space and time complexity at runtime with *no* concomitant benefits.
'''

# ....................{ PRIVATE ~ init }....................
def _init() -> None:
'''
Expand Down
23 changes: 22 additions & 1 deletion beartype/_data/hint/datahinttyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
# ....................{ IMPORTS }....................
from ast import AST
from beartype.typing import (
# TYPE_CHECKING,
AbstractSet,
Any,
Callable,
Dict,
ForwardRef,
Iterable,
List,
Literal,
Expand Down Expand Up @@ -310,6 +311,12 @@
to the :func:`isinstance` and :func:`issubclass` builtins.
'''


SetOrTupleTypes = Union[AbstractSet[type], TupleTypes]
'''
PEP-compliant type hint matching a set *or* tuple of zero or more classes.
'''

# ....................{ TUPLE ~ stack }....................
TypeStack = Optional[Tuple[type, ...]]
'''
Expand Down Expand Up @@ -426,6 +433,20 @@ class hierarchy on the current call stack (if any) by leveraging the total
(i.e., both floating-point numbers and integers).
'''

# ....................{ PEP (484|585) }....................
# Type hints required to fully comply with both PEP 484 *AND* 585.

Pep484585ForwardRef = Union[str, ForwardRef]
'''
Union of all :pep:`484`- or :pep:`585`-compliant **forward reference types**
(i.e., classes of all forward reference objects).
See Also
--------
:data:`HINT_PEP484585_FORWARDREF_TYPES`
Further details.
'''

# ....................{ TYPE }....................
TypeException = Type[Exception]
'''
Expand Down
Loading

0 comments on commit fd23bd9

Please sign in to comment.