Skip to content

Commit

Permalink
Forward reference issubclass() proxying x 2.
Browse files Browse the repository at this point in the history
This commit is the last in a commit chain generalizing our support for
subscripted forward references (e.g., `"type[MuhClass]"`) to proxy both
`isinstance()` *and* `issubclass()` type-checks, resolving issue #289
kindly submitted by Google X extraordinaire @patrick-kidger (Patrick
Kidger). Previously, subscripted forward references erroneously proxied
only `isinstance()` type-checks; this omission prevented these
references from correctly resolving subscriptions of the PEP
585-compliant `type[...]` builtin by a forward reference to a class that
has yet to be defined. Specifically, this commit:

* Improves forward reference proxies to additionally proxy both the
  fully-qualified names of the modules declaring the classes to which
  they refer *and* the unqualified basenames of those classes. Doing so
  substantially improves the readability of exception messages involving
  forward reference proxies.
* Exhaustively unit tests this generalization.

Praise be to the Kidger. (*Sole enmity or an itty-bitty solemnity?*)
  • Loading branch information
leycec committed Oct 7, 2023
1 parent e4ca4ca commit ad0757c
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 87 deletions.
199 changes: 120 additions & 79 deletions beartype/_check/forward/_fwdref.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
TupleTypes,
)
from beartype._check.forward.fwdtype import bear_typistry
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.cache.utilcachecall import (
callable_cached,
property_cached,
)
from beartype._util.cls.utilclsmake import make_type
from beartype._util.cls.utilclstest import is_type_subclass

# ....................{ METACLASSES }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -81,7 +83,7 @@ def __getattr__( # type: ignore[misc]
reference subclass.
Returns
----------
-------
Type['_BeartypeForwardRefIndexableABC']
Fully-qualified forward reference subclass concatenated as described
above.
Expand Down Expand Up @@ -139,7 +141,7 @@ class variable is the fully-qualified name of that external class).
referenced by this forward reference subclass.
Returns
----------
-------
bool
:data:`True` only if this object is an instance of the external
class referenced by this forward reference subclass.
Expand Down Expand Up @@ -170,7 +172,7 @@ class variable is the fully-qualified name of that external class).
referenced by this forward reference subclass.
Returns
----------
-------
bool
:data:`True` only if this object is a subclass of the external class
referenced by this forward reference subclass.
Expand Down Expand Up @@ -225,6 +227,34 @@ def __repr__( # type: ignore[misc]
# Return this representation.
return cls_repr

# ....................{ PROPERTIES }....................
@property # type: ignore[misc]
@property_cached
def __beartype_type__(cls) -> type:
'''
Type hint referenced by this forward reference subclass.
This class property is memoized on its first access for efficiency.
'''

# Fully-qualified name of that class, defined as either...
type_name = (
# If that name already contains one or more "." delimiters and
# is thus presumably already fully-qualified, that name as is;
cls.__beartype_name__
if '.' in cls.__beartype_name__ else # type: ignore[operator]
# Else, that name contains *NO* "." delimiters and is thus
# unqualified. In this case, canonicalize that name into a
# fully-qualified name relative to the fully-qualified name of
# the scope presumably declaring that class.
f'{cls.__beartype_scope_name__}.{cls.__beartype_name__}'
)

# Resolve that class by deferring to our existing "bear_typistry"
# dictionary, which already performs lookup-based resolution and
# caching of arbitrary forward references at runtime.
return bear_typistry[type_name]

# ....................{ SUPERCLASSES }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# CAUTION: The names of *ALL* class variables declared below *MUST* be both:
Expand Down Expand Up @@ -261,8 +291,8 @@ class variable is the name of the attribute referenced by that reference.
'''
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:`__beartype_name` is relative) *or* simply ignored otherwise
(i.e., if :attr:`__beartype_name` is absolute).
(i.e., if :attr:`__beartype_name__` is relative) *or* ignored otherwise
(i.e., if :attr:`__beartype_name__` is absolute).
'''


Expand All @@ -273,17 +303,17 @@ class variable is the name of the attribute referenced by that reference.
'''


__beartype_type__: 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.
'''
# __beartype_type__: 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.
# '''

# ....................{ INITIALIZERS }....................
def __new__(cls, *args, **kwargs) -> NoReturn:
Expand Down Expand Up @@ -316,9 +346,9 @@ class referred to by this forward reference.
class referred to by this forward reference subclass.
'''

# Resolve the external class referred to by this forward reference and
# permanently store that class in the "__beartype_type__" variable.
cls.__beartype_resolve_type__()
# # Resolve the external class referred to by this forward reference and
# # permanently store that class in the "__beartype_type__" variable.
# cls.__beartype_resolve_type__()

# Return true only if this object is an instance of the external class
# referenced by this forward reference.
Expand All @@ -343,56 +373,59 @@ class referred to by this forward reference.
referred to by this forward reference subclass.
'''

# Resolve the external class referred to by this forward reference and
# permanently store that class in the "__beartype_type__" variable.
cls.__beartype_resolve_type__()
# # Resolve the external class referred to by this forward reference and
# # permanently store that class in the "__beartype_type__" variable.
# cls.__beartype_resolve_type__()

# Return true only if this object is a subclass of the external class
# referenced by this forward reference.
return is_type_subclass(obj, cls.__beartype_type__) # type: ignore[arg-type]
return issubclass(obj, cls.__beartype_type__) # type: ignore[arg-type]

# ....................{ PRIVATE ~ resolvers }....................
#FIXME: [SPEED] Optimize this by refactoring this into a cached class
#property defined on the metaclass of the superclass instead. Since doing so
#is a bit non-trivial and nobody particularly cares, the current naive
#approach certainly suffices for now. *sigh*
@classmethod
def __beartype_resolve_type__(cls) -> None:
'''
**Resolve** (i.e., dynamically lookup) the external class referred to by
this forward reference and permanently store that class in the
:attr:`__beartype_type__` class variable for subsequent lookup.
Caveats
-------
This method should *always* be called before accessing the
:attr:`__beartype_type__` class variable, which should *always* be
assumed to be :data:`None` before calling this method.
'''

# If the external class referenced by this forward reference has yet to
# be resolved, do so now.
if cls.__beartype_type__ is None:
# Fully-qualified name of that class, defined as either...
type_name = (
# If that name already contains one or more "." delimiters and
# is thus presumably already fully-qualified, that name as is;
cls.__beartype_name__
if '.' in cls.__beartype_name__ else
# Else, that name contains *NO* "." delimiters and is thus
# unqualified. In this case, canonicalize that name into a
# fully-qualified name relative to the fully-qualified name of
# the scope presumably declaring that class.
f'{cls.__beartype_scope_name__}.{cls.__beartype_name__}'
)

# Resolve that class by deferring to our existing "bear_typistry"
# dictionary, which already performs lookup-based resolution and
# caching of arbitrary forward references at runtime.
cls.__beartype_type__ = bear_typistry[type_name]
# Else, that class has already been resolved.
#
# In either case, that class is now resolved.
#
#On doing so, note that we'll also need to disable this line below:
# forwardref_subtype.__beartype_type__ = None # pyright: ignore[reportGeneralTypeIssues]
# @classmethod
# def __beartype_resolve_type__(cls) -> None:
# '''
# **Resolve** (i.e., dynamically lookup) the external class referred to by
# this forward reference and permanently store that class in the
# :attr:`__beartype_type__` class variable for subsequent lookup.
#
# Caveats
# -------
# This method should *always* be called before accessing the
# :attr:`__beartype_type__` class variable, which should *always* be
# assumed to be :data:`None` before calling this method.
# '''
#
# # If the external class referenced by this forward reference has yet to
# # be resolved, do so now.
# if cls.__beartype_type__ is None:
# # Fully-qualified name of that class, defined as either...
# type_name = (
# # If that name already contains one or more "." delimiters and
# # is thus presumably already fully-qualified, that name as is;
# cls.__beartype_name__
# if '.' in cls.__beartype_name__ else
# # Else, that name contains *NO* "." delimiters and is thus
# # unqualified. In this case, canonicalize that name into a
# # fully-qualified name relative to the fully-qualified name of
# # the scope presumably declaring that class.
# f'{cls.__beartype_scope_name__}.{cls.__beartype_name__}'
# )
#
# # Resolve that class by deferring to our existing "bear_typistry"
# # dictionary, which already performs lookup-based resolution and
# # caching of arbitrary forward references at runtime.
# cls.__beartype_type__ = bear_typistry[type_name]
# # Else, that class has already been resolved.
# #
# # In either case, that class is now resolved.

# ....................{ SUPERCLASSES ~ index }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -473,7 +506,6 @@ def __class_getitem__(cls, *args, **kwargs) -> (
_make_forwardref_subtype( # type: ignore[assignment]
scope_name=cls.__beartype_scope_name__,
hint_name=cls.__beartype_name__,
type_name='_BeartypeForwardRefIndexed',
type_bases=_BeartypeForwardRefIndexedABC_BASES,
))

Expand All @@ -499,12 +531,9 @@ def __class_getitem__(cls, *args, **kwargs) -> (
'''

# ....................{ FACTORIES }....................
#FIXME: Unit test us up, please.
@callable_cached
def make_forwardref_indexable_subtype(
scope_name: str,
hint_name: str,
) -> Type[_BeartypeForwardRefIndexableABC]:
scope_name: str, hint_name: str) -> Type[_BeartypeForwardRefIndexableABC]:
'''
Create and return a new **subscriptable forward reference subclass** (i.e.,
concrete subclass of the :class:`._BeartypeForwardRefIndexableABC`
Expand Down Expand Up @@ -535,18 +564,13 @@ def make_forwardref_indexable_subtype(
return _make_forwardref_subtype( # type: ignore[return-value]
scope_name=scope_name,
hint_name=hint_name,
type_name='_BeartypeForwardRefIndexable',
type_bases=_BeartypeForwardRefIndexableABC_BASES,
)

# ....................{ PRIVATE ~ factories }....................
#FIXME: Unit test us up, please.
def _make_forwardref_subtype(
scope_name: str,
hint_name: str,
type_name: str,
type_bases: TupleTypes,
) -> Type[_BeartypeForwardRefABC]:
scope_name: str, hint_name: str, type_bases: TupleTypes) -> Type[
_BeartypeForwardRefABC]:
'''
Create and return a new **forward reference subclass** (i.e., concrete
subclass of the passed abstract base class (ABC) deferring the resolution of
Expand All @@ -564,8 +588,6 @@ def _make_forwardref_subtype(
hint_name : str
Absolute (i.e., fully-qualified) or relative (i.e., unqualified) name of
the type hint referenced by this forward reference subclass.
type_name : str
Name of the subclass to be created.
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
Expand All @@ -582,11 +604,30 @@ def _make_forwardref_subtype(
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.
type_module_name, _, type_name = hint_name.rpartition('.')

# If this module name is the empty string, this type hint is a relative
# forward reference relative to the passed fully-qualified name of the
# lexical scope. In this case, that scope should be the desired module.
if not type_module_name:
type_module_name = scope_name
# Else, this module name is non-empty.

# Forward reference subclass to be returned.
forwardref_subtype: Type[_BeartypeForwardRefIndexableABC] = make_type(
type_name=type_name,
# Fully-qualified name of the current submodule.
type_module_name=__name__,
type_module_name=type_module_name,
type_bases=type_bases,
)

Expand All @@ -595,7 +636,7 @@ def _make_forwardref_subtype(
forwardref_subtype.__beartype_scope_name__ = scope_name # pyright: ignore[reportGeneralTypeIssues]

# Nullify all remaining class variables of this subclass for safety.
forwardref_subtype.__beartype_type__ = None # pyright: ignore[reportGeneralTypeIssues]
# forwardref_subtype.__beartype_type__ = None # pyright: ignore[reportGeneralTypeIssues]

# Return this subclass.
return forwardref_subtype
18 changes: 13 additions & 5 deletions beartype/_util/cache/utilcachecall.py
Original file line number Diff line number Diff line change
Expand Up @@ -580,16 +580,24 @@ def property_cached(func: _CallableT) -> _CallableT:
return local_attrs['property_method_cached']

# ....................{ PRIVATE ~ constants : var }....................
_CALLABLE_CACHED_VAR_NAME_PREFIX = '_beartype_cached__'
_CALLABLE_CACHED_VAR_NAME_PREFIX = '__beartype_cached__'
'''
Substring prefixing the names of all private instance variables to which all
caching decorators (e.g., :func:`property_cached`) cache values returned by
decorated callables.
This prefix guarantees uniqueness across *all* instances -- including those
instantiated from official Python and unofficial third-party classes and those
internally defined by this application. Doing so permits logic elsewhere (e.g.,
pickling filtering) to uniquely match and act upon these variables.
This prefix:
* Guarantees uniqueness across *all* instances -- including those instantiated
from official Python and unofficial third-party classes and those internally
defined by this application. Doing so permits logic elsewhere (e.g., pickling
filtering) to uniquely match and act upon these variables.
* Is intentionally prefixed by double rather than single underscores (i.e.,
``"__"`` rather than ``"_"``), ensuring that our
:meth:`beartype._check.forward._fwdref._BeartypeForwardRefMeta.__getattr__`
dunder method ignores the private instance variables cached by our cached
:meth:`beartype._check.forward._fwdref._BeartypeForwardRefMeta.__beartype_type__`
property.
'''


Expand Down
Empty file.

0 comments on commit ad0757c

Please sign in to comment.