Skip to content

Commit

Permalink
Forward reference proxy cache handling.
Browse files Browse the repository at this point in the history
This commit significantly improves internal handling of cached forward
reference proxies, particularly with respect to **module reloading**
(i.e., redefinition of classes in modules manually reloaded by external
users and frameworks). Doing so resolves the last remaining unit test
recently broken by the codebase-wide refactoring of forward references.
Our upcoming @beartype 0.17.1 patch release is now good to go. Go, go!
(*Undulating gorgon or unduly ungulate!?*)
  • Loading branch information
leycec committed Feb 9, 2024
1 parent db7d538 commit 2fda008
Show file tree
Hide file tree
Showing 13 changed files with 144 additions and 147 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/code/codescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
Tuple,
)
from beartype._cave._cavemap import NoneTypeOr
from beartype._check.forward.fwdtype import (
from beartype._check.forward.fwdcache import (
TYPISTRY_HINT_NAME_TUPLE_PREFIX,
bear_typistry,
)
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 @@ -426,7 +426,7 @@ def clear_coerce_hint_caches() -> None:
dictionary).
'''

# Clear our type hint cache.
# Clear the type hint cache.
_HINT_REPR_TO_SINGLETON.clear()

# ....................{ PRIVATE ~ mappings }....................
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,58 @@
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,
BeartypeDecorHintForwardRefException,
)
from beartype.roar import BeartypeCallHintForwardRefException
from beartype.roar._roarexc import _BeartypeDecorBeartypistryException
from beartype._check.checkmagic import ARG_NAME_TYPISTRY
from beartype._util.cache.utilcachecall import callable_cached
from beartype._check.forward.reference.fwdrefmake import (
_forwardref_args_to_forwardref)
from beartype._check.forward.reference.fwdrefmeta import (
_forwardref_to_referee)
from beartype._util.cls.pep.utilpep3119 import die_unless_type_isinstanceable
from beartype._util.cls.utilclstest import is_type_builtin
from beartype._util.module.utilmodimport import import_module_attr
from beartype._util.module.utilmodtest import die_unless_module_attr_name
from beartype._util.utilobject import get_object_type_name
from beartype._util.hint.pep.proposal.pep484585.utilpep484585generic import (
is_hint_pep484585_generic,
get_hint_pep484585_generic_type,
)

# ....................{ CLEARERS }....................
def clear_forwardref_caches() -> None:
'''
Clear (i.e., empty) *all* internal caches specifically leveraged by the
:mod:`beartype._check.forward` subpackage, enabling callers to reset this
subpackage to its initial state.
Notably, this function clears:
* The **forward reference proxy cache** (i.e., private
:data:`beartype._check.forward.reference.fwdrefmake._forwardref_args_to_forwardref`
dictionary).
* The **forward reference referee cache** (i.e., private
:data:`beartype._check.forward.reference.fwdrefmeta._forwardref_to_referee`
dictionary).
* The **tuple union cache** (i.e., private :data:`.bear_typistry`
dictionary).
'''

# Clear all forward reference caches.
_forwardref_to_referee.clear()
_forwardref_args_to_forwardref.clear()

# Clear the tuple union cache.
bear_typistry.clear()

# ....................{ CONSTANTS }....................
#FIXME: *DRAMATICALLY SIMPLIFY,* please. Neither this nor the antiquated
#"_Beartypistry" class should be required, anymore. Instead, all we require is
#to refactor all usage of "TYPISTRY_HINT_NAME_TUPLE_PREFIX" by:
#* Defining a new "type_union_hash_to_type_union: Dict[int, TupleTypes] = {}"
# global dictionary of this submodule.
#* Removing "TYPISTRY_HINT_NAME_TUPLE_PREFIX".
#* Removing "_Beartypistry".
#* Removing "bear_typistry".
TYPISTRY_HINT_NAME_TUPLE_PREFIX = '+'
'''
**Beartypistry tuple key prefix** (i.e., substring prefixing the keys of all
Expand All @@ -42,60 +73,9 @@
values are types from pairs whose values are tuples.
'''

# ....................{ REGISTRARS }....................
#FIXME: Unit test us up.
@callable_cached
def make_code_resolve_ref_type(hint_name: str) -> str:
'''
Python expression evaluating to the type hint referred to by the passed
**fully-qualified forward reference** (i.e., absolute ``"."``-delimited name
of a type hint or class that typically has yet to be defined).
This getter creates and returns a Python expression dynamically accessing
this type hint with the private ``__beartypistry`` parameter implicitly
passed to most type-checking wrapper functions generated by the
:func:`beartype.beartype` decorator.
This getter is memoized for efficiency.
Parameters
----------
hint_name : str
Forward reference to be dereferenced.
Returns
-------
str
Python expression evaluating to the type hint referred to by this
forward reference when accessed via the ``__beartypistry`` parameter
passed to wrapper functions generated by :func:`beartype.beartype`.
Raises
------
BeartypeDecorHintForwardRefException
If this forward reference is *not* a syntactically valid fully-qualified
Python identifier containing at least one ``"."`` character.
'''

# If this object is *NOT* the syntactically valid fully-qualified name of a
# module attribute which may *NOT* actually exist, raise an exception.
die_unless_module_attr_name(
module_attr_name=hint_name,
exception_cls=BeartypeDecorHintForwardRefException,
exception_prefix='Forward reference ',
)

# Return a Python expression evaluating to this type *WITHOUT* explicitly
# registering this forward reference with the beartypistry singleton. Why?
# Because the _Beartypistry.__missing__() dunder method implicitly handles
# forward references by dynamically registering types on their first access
# if *NOT* already registered. Ergo, our job is actually done here.
return (
f'{_CODE_TYPISTRY_HINT_NAME_TO_HINT_PREFIX}{repr(hint_name)}'
f'{_CODE_TYPISTRY_HINT_NAME_TO_HINT_SUFFIX}'
)

# ....................{ SUBCLASSES }....................
#FIXME: *DRAMATICALLY SIMPLIFY,* please. See commentary for
#"TYPISTRY_HINT_NAME_TUPLE_PREFIX" above.
class _Beartypistry(dict):
'''
**Beartypistry** (i.e., singleton dictionary mapping from strings uniquely
Expand Down
14 changes: 0 additions & 14 deletions beartype/_check/forward/reference/fwdrefabc.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,6 @@ class variable is the name of the attribute referenced by that reference.
(i.e., if :attr:`__name_beartype__` is absolute).
'''


__type_imported_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 either the
:func:`isinstance` or :func:`issubclass` builtins (i.e., as the first
parameter to the :meth:`.BeartypeForwardRefMeta.__instancecheck__` or
:meth:`.BeartypeForwardRefMeta.__subclasscheck__` dunder methods) *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
49 changes: 23 additions & 26 deletions beartype/_check/forward/reference/fwdrefmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@
from beartype.typing import (
Dict,
Optional,
Tuple,
Type,
)
from beartype._cave._cavemap import NoneTypeOr
from beartype._data.hint.datahinttyping import (
BeartypeForwardRef,
BeartypeForwardRefArgs,
TupleTypes,
)
from beartype._check.forward.reference.fwdrefabc import (
Expand All @@ -33,25 +33,6 @@
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 }....................
def make_forwardref_indexable_subtype(
scope_name: Optional[str],
Expand Down Expand Up @@ -158,13 +139,13 @@ def _make_forwardref_subtype(
'''

# Tuple of all passed parameters (in arbitrary order).
args = (scope_name, hint_name, type_bases)
args: BeartypeForwardRefArgs = (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)
forwardref_subtype = _forwardref_args_to_forwardref.get(args, None)

# If this proxy has already been created, reuse and return this proxy as is.
if forwardref_subtype is not None:
Expand Down Expand Up @@ -214,12 +195,28 @@ def _make_forwardref_subtype(
forwardref_subtype.__name_beartype__ = hint_name # pyright: ignore
forwardref_subtype.__scope_name_beartype__ = scope_name # pyright: ignore

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

# 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
_forwardref_args_to_forwardref[args] = forwardref_subtype

# Return this proxy.
return forwardref_subtype

# ....................{ PRIVATE ~ globals }....................
_forwardref_args_to_forwardref: Dict[
BeartypeForwardRefArgs, 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
proxy 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.
'''
37 changes: 29 additions & 8 deletions beartype/_check/forward/reference/fwdrefmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeCallHintForwardRefException
from beartype.typing import Type
from beartype.typing import Dict
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 (
Expand All @@ -24,8 +24,6 @@
from beartype._util.module.utilmodimport import import_module_attr
from beartype._util.text.utiltextidentifier import is_dunder

# ....................{ PRIVATE ~ hints }....................

# ....................{ METACLASSES }....................
class BeartypeForwardRefMeta(type):
'''
Expand Down Expand Up @@ -262,13 +260,18 @@ def __type_beartype__(cls: BeartypeForwardRef) -> type: # type: ignore[misc]
* This forward referee is importable but either:
* Not a type.
* A type that is this forward reference subclass, implying this
subclass circularly proxies itself.
* A type that is this forward reference proxy, implying this proxy
circularly proxies itself.
'''

# Forward referee referred to by this forward reference proxy if a prior
# access of this property has already resolved this referee *OR* "None"
# otherwise (i.e., if this is the first access of this property).
referee = _forwardref_to_referee.get(cls)

# If this forward referee has yet to be resolved, this is the first call
# to this property. In this case...
if cls.__type_imported_beartype__ is None: # type: ignore[has-type]
if referee is None: # type: ignore[has-type]
# print(f'Importing forward ref "{cls.__name_beartype__}" from module "{cls.__scope_name_beartype__}"...')

# Forward referee dynamically imported from this module.
Expand Down Expand Up @@ -311,10 +314,28 @@ def __type_beartype__(cls: BeartypeForwardRef) -> type: # type: ignore[misc]
# Else, this referee is an isinstanceable class.

# Cache this referee for subsequent lookup by this property.
cls.__type_imported_beartype__ = referee
_forwardref_to_referee[cls] = referee
# Else, this referee has already been resolved.
#
# In either case, this referee is now resolved.

# Return this previously resolved referee.
return cls.__type_imported_beartype__ # type: ignore[return-value]
return referee # type: ignore[return-value]

# ....................{ PRIVATE ~ globals }....................
_forwardref_to_referee: Dict[BeartypeForwardRef, type] = {}
'''
**Forward reference referee cache** (i.e., dictionary mapping from each forward
reference proxy to the arbitrary class referred to by that proxy).
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.
* The
:attr:`.BeartypeForwardRefMeta.__type_beartype__` property to internally
memoize the arbitrary class referred to by this referee. Since the existing
``property_cached`` decorator could trivially do so as well, however, this is
only a negligible side effect.
'''
29 changes: 19 additions & 10 deletions beartype/_data/hint/datahinttyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,6 @@
(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 Expand Up @@ -400,6 +390,25 @@ class hierarchy on the current call stack (if any) by leveraging the total
metadata, as trivially provided by the length of this tuple.
'''

# ....................{ MODULE ~ 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).
'''


BeartypeForwardRefArgs = Tuple[Optional[str], str, TupleTypes]
'''
PEP-compliant type hint matching a **forward reference proxy argument list**
(i.e., tuple of all parameters passed to each call of the low-level private
:func:`beartype._check.forward.reference.fwdrefmake._make_forwardref_subtype`
factory function, in the same order as positionally accepted by that function).
'''

# ....................{ MODULE ~ importlib }....................
# Type hints specific to the standard "importlib" package.

Expand Down

0 comments on commit 2fda008

Please sign in to comment.