Skip to content

Commit

Permalink
PEP 563 + typing.NamedTuple x 7.
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 globally refactors
away our antiquated **beartypistry type cache** (managed by the
`beartype._check.forward.fwdtype` submodule) in favour of our
dramatically preferable **forward reference proxy API** (defined by the
`beartype._check.forward.reference` subpackage). Sadly, doing so broke
one unit test that has yet to be repaired. Sadness flows like wine.
(*Stable but rat-like ratio is unlikeable!*)
  • Loading branch information
leycec committed Feb 5, 2024
1 parent 657a3b6 commit 15b1d1f
Show file tree
Hide file tree
Showing 19 changed files with 558 additions and 396 deletions.
1 change: 1 addition & 0 deletions beartype/_check/checkmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
'''


#FIXME: Excise us up, pleas. This should no longer be required.
ARG_NAME_TYPISTRY = f'{NAME_PREFIX}typistry'
'''
Name of the **private beartypistry parameter** (i.e., :mod:`beartype`-specific
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 @@ -31,7 +31,7 @@
#
#Specifically, rather than accept "typing" nonsense verbatim, we could instead:
#* Detect PEP 544-compatible protocol type hints *NOT* decorated by
# @typing.runtime_checkable. The existing is_type_or_types_isinstanceable() tester now
# @typing.runtime_checkable. The existing is_type_isinstanceable() tester now
# detects whether arbitrary classes are isinstanceable, so just call that.
#* Emit a non-fatal warning advising the end user to resolve this on their end.
#* Meanwhile, beartype can simply:
Expand Down
4 changes: 3 additions & 1 deletion beartype/_check/forward/fwdtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
This private submodule is *not* intended for importation by downstream callers.
'''

#FIXME: Excise this entire submodule, please. This should no longer be required.

# ....................{ IMPORTS }....................
from beartype.roar import (
BeartypeCallHintForwardRefException,
Expand Down Expand Up @@ -324,7 +326,7 @@ def __missing__(self, hint_name: str) -> type:
# Type hint whose fully-qualified name is this forward reference,
# dynamically imported here at presumably callable call-time.
hint = import_module_attr(
module_attr_name=hint_name,
attr_name=hint_name,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)
Expand Down
30 changes: 9 additions & 21 deletions beartype/_check/forward/reference/fwdrefabc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class hierarchy deferring the resolution of a stringified type hint referencing
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype.typing import (
NoReturn,
Optional,
Type,
)
from beartype._data.hint.datahinttyping import (
Expand Down Expand Up @@ -55,33 +56,20 @@ class variable is the name of the attribute referenced by that reference.
'''

# ....................{ PRIVATE ~ class vars }....................
__scope_name_beartype__: str = None # type: ignore[assignment]
'''
Fully-qualified name of the lexical scope to which the type hint referenced
by this forward reference subclass is relative if that type hint is relative
(i.e., if :attr:`__name_beartype__` is relative) *or* ignored otherwise
(i.e., if :attr:`__name_beartype__` is absolute).
'''


__name_beartype__: str = None # type: ignore[assignment]
'''
Absolute (i.e., fully-qualified) or relative (i.e., unqualified) name of the
type hint referenced by this forward reference subclass.
'''


# __type_beartype__: Optional[type] = None
# '''
# Type hint referenced by this forward reference subclass if this subclass has
# already been passed at least once as the second parameter to the
# :func:`isinstance` builtin (i.e., as the first parameter to the
# :meth:`.BeartypeForwardRefMeta.__instancecheck__` dunder method and
# :meth:`is_instance` method) *or* :data:`None` otherwise.
#
# Note that this class variable is an optimization reducing space and time
# complexity for subsequent lookup of this same type hint.
# '''
__scope_name_beartype__: Optional[str] = None
'''
Fully-qualified name of the lexical scope to which the type hint referenced
by this forward reference subclass is relative if that type hint is relative
(i.e., if :attr:`__name_beartype__` is relative) *or* ignored otherwise
(i.e., if :attr:`__name_beartype__` is absolute).
'''

# ....................{ INITIALIZERS }....................
def __new__(cls, *args, **kwargs) -> NoReturn:
Expand Down Expand Up @@ -276,8 +264,8 @@ def __class_getitem__(cls, *args, **kwargs) -> (
# _make_forwardref_subtype() factory function.
forwardref_indexed_subtype: Type[_BeartypeForwardRefIndexedABC] = (
_make_forwardref_subtype( # type: ignore[assignment]
scope_name=cls.__scope_name_beartype__,
hint_name=cls.__name_beartype__,
scope_name=cls.__scope_name_beartype__,
type_bases=_BeartypeForwardRefIndexedABC_BASES,
))

Expand Down
78 changes: 49 additions & 29 deletions beartype/_check/forward/reference/fwdrefmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype.typing import (
Optional,
Type,
)
from beartype._data.hint.datahinttyping import (
Expand All @@ -28,11 +29,14 @@
)
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

# ....................{ FACTORIES }....................
@callable_cached
def make_forwardref_indexable_subtype(
scope_name: str, hint_name: str) -> Type[_BeartypeForwardRefIndexableABC]:
scope_name: Optional[str],
hint_name: str,
) -> Type[_BeartypeForwardRefIndexableABC]:
'''
Create and return a new **subscriptable forward reference subclass** (i.e.,
concrete subclass of the :class:`._BeartypeForwardRefIndexableABC` abstract
Expand All @@ -42,7 +46,7 @@ def make_forwardref_indexable_subtype(
Parameters
----------
scope_name : str
scope_name : Optional[str]
Possibly ignored lexical scope name. Specifically:
* If ``hint_name`` is absolute (i.e., contains one or more ``.``
Expand All @@ -66,6 +70,17 @@ def make_forwardref_indexable_subtype(
-------
Type[_BeartypeForwardRefIndexableABC]
Subscriptable forward reference subclass referencing this type hint.
Raises
------
BeartypeDecorHintForwardRefException
If either:
* ``hint_name`` is *not* a syntactically valid Python identifier.
* ``scope_name`` is neither:
* A syntactically valid Python identifier.
* :data:`None`.
'''

# Subscriptable forward reference to be returned.
Expand All @@ -80,7 +95,7 @@ def make_forwardref_indexable_subtype(

# ....................{ PRIVATE ~ factories }....................
def _make_forwardref_subtype(
scope_name: str,
scope_name: Optional[str],
hint_name: str,
type_bases: TupleTypes,
) -> Type[BeartypeForwardRefABC]:
Expand All @@ -95,7 +110,7 @@ def _make_forwardref_subtype(
Parameters
----------
scope_name : str
scope_name : Optional[str]
Possibly ignored lexical scope name. See
:func:`.make_forwardref_indexable_subtype` for further details.
hint_name : str
Expand All @@ -115,45 +130,50 @@ def _make_forwardref_subtype(
Raises
------
BeartypeDecorHintForwardRefException
If this is a **relative forward reference** (i.e., ``hint_name``
contains *no* ``.`` delimiters) *and* ``scope_name`` is :data:`None`,
preventing this reference from being canonicalized into an absolute
forward reference.
If either:
* ``hint_name`` is *not* a syntactically valid Python identifier.
* ``scope_name`` is neither:
* A syntactically valid Python identifier.
* :data:`None`.
'''
assert isinstance(hint_name, str), f'{repr(hint_name)} not string.'
assert isinstance(scope_name, str), f'{repr(scope_name)} not string.'
assert len(type_bases) == 1, (
f'{repr(type_bases)} not 1-tuple of a single superclass.')

# Fully-qualified module name *AND* unqualified basename of the type hint
# referenced by this forward reference subclass. Specifically, if the name
# of this type hint is:
# * Fully-qualified:
# * This module name is the substring of this name preceding the last "."
# delimiter in this name.
# * This basename is the substring of this name following the last "."
# delimiter in this name.
# * Unqualified:
# * This module name is the empty string and thus ignorable.
# * This basename is this name as is.
# If this attribute name is *NOT* a syntactically valid Python identifier,
# raise an exception.
die_unless_identifier(
text=hint_name,
exception_cls=BeartypeDecorHintForwardRefException,
exception_prefix='Forward reference ',
)
# Else, this attribute name is a syntactically valid Python identifier.

# Possibly empty fully-qualified module name and unqualified basename of the
# type referred to by this forward reference.
type_module_name, _, type_name = hint_name.rpartition('.')
# _, _, type_name = hint_name.rpartition('.')

# If this module name is the empty string *AND* no lexical scope name was
# passed, this type hint is a relative forward reference relative to *NO*
# lexical scope. In this case, raise an exception.
if not (scope_name or type_module_name):
# If this module name is empty, fallback to the passed module name if any.
#
# Note that we intentionally perform *NO* additional validation. Why?
# Builtin types. Notably, it is valid to pass an unqualified "hint_name"
# and a "scope_name" that is "None" only if "hint_name" is the name of a
# builtin type (e.g., "int", "str"). Since validating this edge case is
# non-trivial, we defer this validation to subsequent importation logic.
if not type_module_name:
type_module_name = scope_name
# Else, either this module name is non-empty *OR* a lexical scope name was
# passed. This type hint is thus either already an absolute forward
# reference or a relative forward reference relative to a lexical scope that
# can be canonicalized into an absolute forward reference.
# Else, this module name is non-empty.

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

# Classify passed parameters with this subclass.
Expand Down
83 changes: 44 additions & 39 deletions beartype/_check/forward/reference/fwdrefmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
# ....................{ IMPORTS }....................
from beartype.roar import BeartypeCallHintForwardRefException
from beartype.typing import Type
from beartype._check.forward.fwdtype import bear_typistry
from beartype._check.forward.reference import fwdrefabc # <-- satisfy mypy
from beartype._util.cache.utilcachecall import property_cached
from beartype._util.cls.pep.utilpep3119 import die_unless_type_isinstanceable
from beartype._util.hint.pep.proposal.pep484585.utilpep484585generic import (
is_hint_pep484585_generic,
get_hint_pep484585_generic_type,
)
from beartype._util.module.utilmodimport import import_module_attr
from beartype._util.text.utiltextidentifier import is_dunder

# ....................{ PRIVATE ~ hints }....................
Expand Down Expand Up @@ -91,19 +96,8 @@ class variable is the fully-qualified name of an external class).
from beartype._check.forward.reference.fwdrefmake import (
make_forwardref_indexable_subtype)

#FIXME: Alternately, we might consider explicitly:
#* Defining the set of *ALL* known dunder attributes (e.g., methods,
# class variables). This is non-trivial and error-prone, due to the
# introduction of new dunder attributes across Python versions.
#* Detecting whether this "hint_name" is in that set.
#
#That would have the advantage of supporting forward references
#containing dunder attributes. Until someone actually wants to do that,
#however, let's avoid doing that. The increase in fragility is *BRUTAL*.

# If this unqualified basename is that of a non-existent dunder
# attribute both prefixed *AND* suffixed by the magic substring "__",
# raise the standard "AttributeError" exception.
# attribute, raise the standard "AttributeError" exception.
if is_dunder(hint_name):
raise AttributeError(
f'Forward reference proxy dunder attribute '
Expand Down Expand Up @@ -194,8 +188,8 @@ def __repr__(cls: ForwardRef) -> str: # type: ignore[misc]
# * The die_if_hint_pep604_inconsistent() raiser.
cls_repr = (
f'<forwardref {cls.__name__}('
f'__scope_name_beartype__={repr(cls.__scope_name_beartype__)}'
f', __name_beartype__={repr(cls.__name_beartype__)}'
f'__name_beartype__={repr(cls.__name_beartype__)}'
f', __scope_name_beartype__={repr(cls.__scope_name_beartype__)}'
)

#FIXME: Unit test this edge case, please.
Expand Down Expand Up @@ -241,42 +235,53 @@ def __type_beartype__(cls: ForwardRef) -> type:
Raises
------
BeartypeCallHintForwardRefException
If this forward referee is this forward reference subclass, implying
this subclass circularly proxies itself.
If either:
* This forward referee is unimportable.
* This forward referee is importable but either:
* Not a type.
* A type that is this forward reference subclass, implying this
subclass circularly proxies itself.
'''

# Fully-qualified name of this forward referee (i.e., type hint
# referenced by this forward reference subclass, which is usually but
# *not* necessarily a class), initialized to the existing name as is.
referee_name: str = cls.__name_beartype__

# If this name contains *NO* "." delimiters and is thus unqualified
# (i.e., relative)...
if '.' not in referee_name: # type: ignore[operator]
# Canonicalize this name into a fully-qualified name relative to the
# fully-qualified name of the scope presumably declaring this
# forward referee.
referee_name = f'{cls.__scope_name_beartype__}.{referee_name}'
# Else, this name contains one or more "." delimiters and is thus
# presumably fully-qualified (i.e., absolute).

#FIXME: *NOPE.* Let's obviate the "bear_typistry" entirely by deferring
#to lower-level import_module_attr*() importers, please.
# Forward referee, dynamically resolved by deferring to our existing
# "bear_typistry" dictionary, which already performs lookup-based
# resolution and caching of arbitrary forward references at runtime.
referee = bear_typistry[referee_name]
# Forward referee dynamically imported from this module.
referee = import_module_attr(
attr_name=cls.__name_beartype__,
module_name=cls.__scope_name_beartype__,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)

# If this referee is this forward reference subclass, then this subclass
# circularly proxies itself. Since allowing this edge case would openly
# invite infinite recursion, we detect this edge case and instead raise
# a human-readable exception.
if cls is referee:
if referee is cls:
raise BeartypeCallHintForwardRefException(
f'Forward reference proxy {repr(cls)} '
f'circularly (i.e., infinitely recursively) references itself.'
)
# Else, this referee is *NOT* this forward reference subclass.
#
# If this referee is a subscripted generic (e.g., ``MuhGeneric[int]``),
# reduce this referee to the class subscripting this generic (e.g.,
# "int").
elif is_hint_pep484585_generic(referee):
referee = get_hint_pep484585_generic_type(
hint=referee,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)
# Else, this referee is *NOT* a subscripted generic.

# If this referee is *NOT* an isinstanceable class, raise an exception.
die_unless_type_isinstanceable(
cls=referee,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)
# Else, this referee is an isinstanceable class.

# Return this forward referee.
return referee

0 comments on commit 15b1d1f

Please sign in to comment.