Skip to content

Commit

Permalink
PEP 563 + typing.NamedTuple x 10.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain explicitly supporting
`typing.NamedTuple` subclasses under PEP 563 (i.e., `from __future__
import annotations`), 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. @beartype now generically high strangeness like this:

```python
from __future__ import annotations
from typing import NamedTuple

class HorribleTuple(typing.NamedTuple):
    my_eyes_are_bleeding: int

how_bad_could_it_be = HorribleTuple(0xBADBAD)
```

@beartype took one for the team so that you didn't have to. (*Dithering slithering!*)
  • Loading branch information
leycec committed Feb 8, 2024
1 parent a7794bb commit db7d538
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 267 deletions.
17 changes: 7 additions & 10 deletions beartype/_check/code/codescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@
from beartype._util.cls.utilclstest import is_type_builtin
from beartype._util.func.utilfuncscope import add_func_scope_attr
from beartype._util.hint.pep.proposal.pep484585.utilpep484585ref import (
die_unless_hint_pep484585_ref,
get_hint_pep484585_ref_names,
)
get_hint_pep484585_ref_names)
from beartype._util.utilobject import get_object_type_basename
from collections.abc import Set

Expand Down Expand Up @@ -137,7 +135,7 @@ def add_func_scope_ref(

# Name of a new parameter passing this forward reference proxy.
hint_ref_arg_name = add_func_scope_attr(
attr=hint_ref, func_scope=func_scope)
func_scope=func_scope, attr=hint_ref)

# Return this name.
return hint_ref_arg_name
Expand Down Expand Up @@ -518,13 +516,9 @@ def express_func_scope_type_ref(
BeartypeDecorHintForwardRefException
If this forward reference is *not* actually a forward reference.
'''
assert isinstance(func_scope, dict), f'{repr(func_scope)} not dictionary.'
assert isinstance(forwardrefs_class_basename, NoneTypeOr[set]), (
f'{repr(forwardrefs_class_basename)} neither set nor "None".')

# Possibly undefined fully-qualified module name and possibly
# unqualified classname referred to by this relative forward
# reference, relative to the decorated type stack and callable.
# Possibly undefined fully-qualified module name and possibly unqualified
# classname referred to by this forward reference.
ref_module_name, ref_name = get_hint_pep484585_ref_names(
hint=forwardref, exception_prefix=exception_prefix)

Expand All @@ -549,6 +543,9 @@ def express_func_scope_type_ref(
)
# Else, this classname is unqualified. In this case...
else:
assert isinstance(forwardrefs_class_basename, NoneTypeOr[set]), (
f'{repr(forwardrefs_class_basename)} neither set nor "None".')

# If this set of unqualified classnames referred to by all relative
# forward references has yet to be instantiated, do so.
if forwardrefs_class_basename is None:
Expand Down
4 changes: 2 additions & 2 deletions beartype/_check/error/_errortype.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,8 +193,8 @@ def find_cause_instance_type_forwardref(
# Class referred to by this absolute or relative forward reference.
hint_ref_type = import_pep484585_ref_type(
hint=cause.hint,
func=cause.func,
cls_stack=cause.cls_stack,
func=cause.func,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix=cause.exception_prefix,
)
Expand Down Expand Up @@ -318,8 +318,8 @@ def find_cause_subclass_type(cause: ViolationCause) -> ViolationCause:
# relative forward reference.
hint_superclass = import_pep484585_ref_type(
hint=hint_superclass, # type: ignore[arg-type]
func=cause.func,
cls_stack=cause.cls_stack,
func=cause.func,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix=cause.exception_prefix,
)
Expand Down
3 changes: 0 additions & 3 deletions beartype/_check/forward/reference/fwdrefabc.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,6 @@ def __class_getitem__(cls, *args, **kwargs) -> (
_make_forwardref_subtype)

# Subscripted forward reference to be returned.
#
# Note that parameters *MUST* be passed positionally to the memoized
# _make_forwardref_subtype() factory function.
forwardref_indexed_subtype: Type[_BeartypeForwardRefIndexedABC] = (
_make_forwardref_subtype( # type: ignore[assignment]
hint_name=cls.__name_beartype__,
Expand Down
86 changes: 61 additions & 25 deletions beartype/_check/forward/reference/fwdrefmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,43 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype.typing import (
Dict,
Optional,
Tuple,
Type,
)
from beartype._cave._cavemap import NoneTypeOr
from beartype._data.hint.datahinttyping import (
BeartypeForwardRef,
TupleTypes,
)
from beartype._check.forward.reference.fwdrefabc import (
BeartypeForwardRefABC,
_BeartypeForwardRefIndexableABC,
_BeartypeForwardRefIndexableABC_BASES,
)
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.cls.utilclsmake import make_type
from beartype._util.text.utiltextidentifier import die_unless_identifier

# ....................{ GLOBALS }....................
make_forwardref_subtype_args_to_cls: Dict[
Tuple[Optional[str], str, TupleTypes], BeartypeForwardRef] = {}
'''
**Forward reference proxy cache** (i.e., dictionary mapping from the tuple of
all parameters passed to each prior call of the
:func:`._make_forwardref_subtype` factory function to the forward reference
subclass dynamically created and returned by that call).
This cache serves a dual purpose. Notably, this cache both enables:
* External callers to iterate over all previously instantiated forward reference
proxies. This is particularly useful when responding to module reloading,
which requires that *all* previously cached types be uncached.
* :func:`._make_forwardref_subtype` to internally memoize itself over its
passed parameters. Since the existing ``callable_cached`` decorator could
trivially do so as well, however, this is only a negligible side effect.
'''

# ....................{ FACTORIES }....................
@callable_cached
def make_forwardref_indexable_subtype(
scope_name: Optional[str],
hint_name: str,
Expand All @@ -45,28 +64,31 @@ def make_forwardref_indexable_subtype(
the passed name, transparently permitting this type hint to be subscripted
by any arbitrary positional and keyword parameters).
This factory is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as the lower-level private
:func:`._make_forwardref_subtype` factory called by this higher-level public
factory is itself memoized.
Parameters
----------
scope_name : Optional[str]
Possibly ignored lexical scope name. Specifically:
* If ``hint_name`` is absolute (i.e., contains one or more ``.``
* If "hint_name" is absolute (i.e., contains one or more ``.``
delimiters), this parameter is silently ignored in favour of the
fully-qualified name of the module prefixing ``hint_name``.
* If ``hint_name`` is relative (i.e., contains *no* ``.`` delimiters),
fully-qualified name of the module prefixing "hint_name".
* If "hint_name" is relative (i.e., contains *no* ``.`` delimiters),
this parameter declares the absolute (i.e., fully-qualified) name of
the lexical scope to which this unresolved type hint is relative.
The fully-qualified name of the module prefixing ``hint_name`` (if any)
The fully-qualified name of the module prefixing "hint_name" (if any)
thus *always* takes precedence over this lexical scope name, which only
provides a fallback to resolve relative forward references. While
unintuitive, this is needed to resolve absolute forward references.
hint_name : str
Relative (i.e., unqualified) or absolute (i.e., fully-qualified) name of
this unresolved type hint to be referenced.
This factory is memoized for efficiency.
Returns
-------
Type[_BeartypeForwardRefIndexableABC]
Expand All @@ -85,9 +107,6 @@ def make_forwardref_indexable_subtype(
'''

# Subscriptable forward reference to be returned.
#
# Note that parameters *MUST* be passed positionally to the memoized
# _make_forwardref_subtype() factory function.
return _make_forwardref_subtype( # type: ignore[return-value]
scope_name=scope_name,
hint_name=hint_name,
Expand All @@ -99,15 +118,13 @@ def _make_forwardref_subtype(
scope_name: Optional[str],
hint_name: str,
type_bases: TupleTypes,
) -> Type[BeartypeForwardRefABC]:
) -> BeartypeForwardRef:
'''
Create and return a new **forward reference subclass** (i.e., concrete
subclass of the passed abstract base class (ABC) deferring the resolution of
the type hint with the passed name transparently).
This factory is intentionally *not* memoized (e.g., by the
:func:`callable_cached` decorator), as *all* higher-level public factories
calling this private factory are themselves already memoized.
This factory is internally memoized for efficiency.
Parameters
----------
Expand All @@ -119,13 +136,13 @@ def _make_forwardref_subtype(
the type hint referenced by this forward reference subclass.
type_bases : Tuple[type, ...]
Tuple of all base classes to be inherited by this forward reference
subclass. For simplicity, this *must* be a 1-tuple
``(type_base,)`` where ``type_base`` is a
:class:`._BeartypeForwardRefIndexableABC` subclass.
subclass. For simplicity, this *must* be a 1-tuple ``(type_base,)``
where ``type_base`` is a :class:`._BeartypeForwardRefIndexableABC`
subclass.
Returns
-------
Type[_BeartypeForwardRefIndexableABC]
BeartypeForwardRef
Forward reference subclass referencing this type hint.
Raises
Expand All @@ -139,6 +156,21 @@ def _make_forwardref_subtype(
* A syntactically valid Python identifier.
* :data:`None`.
'''

# Tuple of all passed parameters (in arbitrary order).
args = (scope_name, hint_name, type_bases)

# Forward reference proxy previously created and returned by a prior call to
# this function passed these parameters if any *OR* "None" otherwise (i.e.,
# if this is the first call to this function passed these parameters).
# forwardref_subtype: Optional[BeartypeForwardRef] = (
forwardref_subtype = make_forwardref_subtype_args_to_cls.get(args, None)

# If this proxy has already been created, reuse and return this proxy as is.
if forwardref_subtype is not None:
return forwardref_subtype
# Else, this proxy has yet to be created.

assert isinstance(scope_name, NoneTypeOr[str]), (
f'{repr(scope_name)} neither string nor "None".')
assert isinstance(hint_name, str), f'{repr(hint_name)} not string.'
Expand Down Expand Up @@ -169,21 +201,25 @@ def _make_forwardref_subtype(
type_module_name = scope_name
# Else, this module name is non-empty.

# Forward reference subclass to be returned.
forwardref_subtype: Type[_BeartypeForwardRefIndexableABC] = make_type(
# Forward reference proxy to be returned.
forwardref_subtype = make_type(
type_name=type_name,
type_module_name=type_module_name,
type_bases=type_bases,
exception_cls=BeartypeDecorHintForwardRefException,
exception_prefix='Forward reference ',
)

# Classify passed parameters with this subclass.
# Classify passed parameters with this proxy.
forwardref_subtype.__name_beartype__ = hint_name # pyright: ignore
forwardref_subtype.__scope_name_beartype__ = scope_name # pyright: ignore

# Nullify all remaining class variables of this subclass for safety.
# Nullify all remaining class variables of this proxy for safety.
forwardref_subtype.__type_imported_beartype__ = None # pyright: ignore

# Return this subclass.
# Cache this proxy for reuse by subsequent calls to this factory function
# passed the same parameters.
make_forwardref_subtype_args_to_cls[args] = forwardref_subtype

# Return this proxy.
return forwardref_subtype
21 changes: 7 additions & 14 deletions beartype/_check/forward/reference/fwdrefmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeCallHintForwardRefException
from beartype.typing import Type
from beartype._check.forward.reference import fwdrefabc # <-- satisfy mypy
from beartype._data.hint.datahinttyping import BeartypeForwardRef
from beartype._util.cls.pep.utilpep3119 import die_unless_type_isinstanceable
from beartype._util.hint.pep.proposal.pep484585.utilpep484585generic import (
is_hint_pep484585_generic,
Expand All @@ -25,13 +25,6 @@
from beartype._util.text.utiltextidentifier import is_dunder

# ....................{ PRIVATE ~ hints }....................
ForwardRef = Type['fwdrefabc.BeartypeForwardRefABC']
'''
PEP-compliant type hint matching a **forward reference proxy** (i.e., concrete
subclass of the abstract
:class:`beartype._check.forward.reference.fwdrefabc.BeartypeForwardRefABC`
superclass).
'''

# ....................{ METACLASSES }....................
class BeartypeForwardRefMeta(type):
Expand Down Expand Up @@ -59,8 +52,8 @@ class BeartypeForwardRefMeta(type):
'''

# ....................{ DUNDERS }....................
def __getattr__(cls: ForwardRef, hint_name: str) -> Type[ # type: ignore[misc]
'fwdrefabc._BeartypeForwardRefIndexableABC']:
def __getattr__(cls: BeartypeForwardRef, hint_name: str) -> ( # type: ignore[misc]
BeartypeForwardRef):
'''
**Fully-qualified forward reference subclass** (i.e.,
:class:`.BeartypeForwardRefABC` subclass whose metaclass is this
Expand Down Expand Up @@ -129,7 +122,7 @@ class variable is the fully-qualified name of an external class).
)


def __instancecheck__(cls: ForwardRef, obj: object) -> bool: # type: ignore[misc]
def __instancecheck__(cls: BeartypeForwardRef, obj: object) -> bool: # type: ignore[misc]
'''
:data:`True` only if the passed object is an instance of the external
class referenced by the passed **forward reference subclass** (i.e.,
Expand Down Expand Up @@ -157,7 +150,7 @@ class referenced by this forward reference subclass.
return cls.__is_instance_beartype__(obj)


def __subclasscheck__(cls: ForwardRef, obj: object) -> bool: # type: ignore[misc]
def __subclasscheck__(cls: BeartypeForwardRef, obj: object) -> bool: # type: ignore[misc]
'''
:data:`True` only if the passed object is a subclass of the external
class referenced by the passed **forward reference subclass** (i.e.,
Expand Down Expand Up @@ -186,7 +179,7 @@ class variable is the fully-qualified name of that external class).
return cls.__is_subclass_beartype__(obj)


def __repr__(cls: ForwardRef) -> str: # type: ignore[misc]
def __repr__(cls: BeartypeForwardRef) -> str: # type: ignore[misc]
'''
Machine-readable string representing this forward reference subclass.
'''
Expand Down Expand Up @@ -239,7 +232,7 @@ def __repr__(cls: ForwardRef) -> str: # type: ignore[misc]

# ....................{ PROPERTIES }....................
@property
def __type_beartype__(cls: ForwardRef) -> type: # type: ignore[misc]
def __type_beartype__(cls: BeartypeForwardRef) -> type: # type: ignore[misc]
'''
**Forward referee** (i.e., type hint referenced by this forward
reference subclass, which is usually but *not* necessarily a class).
Expand Down
11 changes: 11 additions & 0 deletions beartype/_data/hint/datahinttyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
'''

# ....................{ IMPORTS }....................
import beartype # <-- satisfy mypy [note to self: i can't stand you, mypy]
from ast import AST
from beartype.typing import (
AbstractSet,
Expand Down Expand Up @@ -53,6 +54,16 @@
(i.e., list of zero or more AST nodes).
'''

# ....................{ BEARTYPE }....................
BeartypeForwardRef = Type[
'beartype._check.forward.reference.fwdrefabc.BeartypeForwardRefABC'] # pyright: ignore
'''
PEP-compliant type hint matching a **forward reference proxy** (i.e., concrete
subclass of the abstract
:class:`beartype._check.forward.reference.fwdrefabc.BeartypeForwardRefABC`
superclass).
'''

# ....................{ BOOL }....................
BoolTristate = Literal[True, False, None]
'''
Expand Down

0 comments on commit db7d538

Please sign in to comment.