Skip to content

Commit

Permalink
Postponed evaluation of PEP 563-compliant type aliases.
Browse files Browse the repository at this point in the history
This commit generalizes @beartype's support for forward references from
**type forward references** (i.e., references to user-defined *types*
that have yet to be defined in the current lexical scope) to
**subscripted generic type alias forward references** (i.e., references
to type aliases (that have yet to be defined in the current lexical
scope and thus require forward references) to subscripted generics that
have been defined), resolving feature request #222 kindly submitted by
Google X JAX "Acronym Maestro" @patrick-kidger (Patrick Kidger).
Specifically, this commit:

* Generalizes our private
  `beartype._check.forward.fwdtype._Beartypistry.__missing__()` dunder
  method to support both:
  * **Subscripted generics** (e.g., `MuhGeneric[int]`).
  * **Isinstanceable types** (e.g., `MuhGeneric`).
* Unit tests this generalization.

(*Subordinate insubordination is notional at best, ordinal!*)
  • Loading branch information
leycec committed Aug 29, 2023
1 parent 4f71159 commit e82a1f7
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 80 deletions.
4 changes: 2 additions & 2 deletions beartype/_check/code/_codescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
from beartype._check.forward.fwdtype import (
TYPISTRY_HINT_NAME_TUPLE_PREFIX,
bear_typistry,
register_typistry_forwardref,
get_hint_forwardref_code,
)
from beartype._check.checkmagic import ARG_NAME_TYPISTRY
from beartype._check.code._codesnip import (
Expand Down Expand Up @@ -509,7 +509,7 @@ def express_func_scope_type_forwardref(

# Python expression evaluating to this class when accessed via this
# private "__beartypistry" attribute.
forwardref_expr = register_typistry_forwardref(forwardref_classname)
forwardref_expr = get_hint_forwardref_code(forwardref_classname)
# Else, this classname is unqualified. In this case...
else:
# If this set of unqualified classnames referred to by all relative
Expand Down
137 changes: 66 additions & 71 deletions beartype/_check/forward/fwdtype.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
This private submodule is *not* intended for importation by downstream callers.
'''

# ....................{ TODO }....................
#FIXME: Consider obsoleting this submodule entirely in favour of the more
#general-purpose and significantly more powerful "cachescope" submodule, please.

# ....................{ IMPORTS }....................
from beartype.roar import (
BeartypeCallHintForwardRefException,
Expand All @@ -28,6 +24,10 @@
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,
)

# ....................{ CONSTANTS }....................
TYPISTRY_HINT_NAME_TUPLE_PREFIX = '+'
Expand All @@ -41,75 +41,60 @@
'''

# ....................{ REGISTRARS ~ forwardref }....................
#FIXME: *UHM.* This function is clearly a vestigial relic from the Dark Times,
#back when the "bear_typistry" singleton did a great deal more than it currently
#does. Notably, this function doesn't actually "register" anything. Both the
#function name *AND* docstring of this function are erroneous red herrings. All
#this function does is return a trivial code snippet. That's it. *facepalm*
#
#At the very least, we should:
#* Rename this function to get_type_forwardref_code().
#* Revise the docstring accordingly, please.
#FIXME: Unit test us up.

# Note this function intentionally does *NOT* accept an optional "hint_labal"
# parameter as doing so would conflict with memoization.
@callable_cached
def register_typistry_forwardref(type_name: str) -> str:
def get_hint_forwardref_code(hint_name: str) -> str:
'''
Register the passed **fully-qualified forward reference** (i.e., string
whose value is the fully-qualified name of a user-defined class that
typically has yet to be defined) with the beartypistry singleton *and*
return a Python expression evaluating to this class when accessed via the
private ``__beartypistry`` parameter implicitly passed to all wrapper
functions generated by the :func:`beartype.beartype` decorator.
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 function is memoized for both efficiency *and* safety, preventing
accidental reregistration.
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
----------
type_name : object
Forward reference to be registered, defined as either:
* A string whose value is the syntactically valid name of a class.
* An instance of the :class:`typing.ForwardRef` class.
hint_name : str
Forward reference to be dereferenced.
Returns
----------
str
Python expression evaluating to the user-defined referred to by this
forward reference when accessed via the private ``__beartypistry``
parameter implicitly passed to all wrapper functions generated by the
:func:`beartype.beartype` decorator.
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 classname.
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=type_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
# 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(type_name)}'
f'{_CODE_TYPISTRY_HINT_NAME_TO_HINT_PREFIX}{repr(hint_name)}'
f'{_CODE_TYPISTRY_HINT_NAME_TO_HINT_SUFFIX}'
)

# ....................{ SUBCLASSES }....................
class Beartypistry(dict):
class _Beartypistry(dict):
'''
**Beartypistry** (i.e., singleton dictionary mapping from strings uniquely
identifying PEP-noncompliant type hints annotating callables decorated
Expand Down Expand Up @@ -144,8 +129,7 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
'''
Dunder method explicitly called by the superclass on setting the passed
key-value pair with ``[``- and ``]``-delimited syntax, mapping the
passed string uniquely identifying the passed PEP-noncompliant type
hint to that hint.
passed string uniquely identifying the passed type hint to that hint.
Parameters
----------
Expand All @@ -157,13 +141,13 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
the module attribute defining this type.
* A tuple of non-:mod:`typing` types, this is a string:
* Prefixed by the :data:`TYPISTRY_HINT_NAME_TUPLE_PREFIX`
* Prefixed by the :data:`.TYPISTRY_HINT_NAME_TUPLE_PREFIX`
substring distinguishing this string from fully-qualified
classnames.
* Hash of these types (ignoring duplicate types and type order in
this tuple).
hint : object
PEP-noncompliant type hint to be mapped from this string.
Type hint to be mapped from this string.
Raises
----------
Expand All @@ -182,7 +166,7 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
name has already been registered, implying a key collision
between the type or tuple already registered under this key and
this passed type or tuple to be reregistered under this key.
Since the high-level :func:`register_typistry_type` and
Since the high-level :func:`.register_typistry_type` and
:func:`register_typistry_tuple` functions implicitly calling
this low-level dunder method are memoized *and* since the
latter function explicitly avoids key collisions by detecting
Expand Down Expand Up @@ -260,7 +244,7 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
# Then raise an exception.
):
raise _BeartypeDecorBeartypistryException(
f'Beartypistry key "{hint_name}" not '
f'Beartypistry key {repr(hint_name)} not '
f'fully-qualified classname "{hint_clsname}" of '
f'type {hint}.'
)
Expand All @@ -284,90 +268,101 @@ def __setitem__(self, hint_name: str, hint: object) -> None:
# guarantees this constraint to be the case.
if not hint_name.startswith(TYPISTRY_HINT_NAME_TUPLE_PREFIX):
raise _BeartypeDecorBeartypistryException(
f'Beartypistry key "{hint_name}" not '
f'Beartypistry key {repr(hint_name)} not '
f'prefixed by "{TYPISTRY_HINT_NAME_TUPLE_PREFIX}" for '
f'tuple {repr(hint)}.'
)
# Else, this hint is neither a class nor a tuple. In this case,
# something has gone terribly awry. Pour out an exception.
else:
raise _BeartypeDecorBeartypistryException(
f'Beartypistry key "{hint_name}" value {repr(hint)} invalid '
f'Beartypistry key {repr(hint_name)} '
f'value {repr(hint)} invalid '
f'(i.e., neither type nor tuple).'
)

# Cache this object under this name.
super().__setitem__(hint_name, hint)


def __missing__(self, hint_classname: str) -> type:
def __missing__(self, hint_name: str) -> type:
'''
Dunder method explicitly called by the superclass
:meth:`dict.__getitem__` method implicitly called on caller attempts to
access the passed missing key with ``[``- and ``]``-delimited syntax.
This method treats this attempt to get this missing key as the
intentional resolution of a forward reference whose fully-qualified
classname is this key.
intentional resolution of a forward reference to a type hint whose
fully-qualified attribute name is this key.
Parameters
----------
hint_classname : str
**Name** (i.e., fully-qualified name of the user-defined class) of
this hint to be resolved as a forward reference.
hint_name : str
Fully-qualified name of the type hint to be resolved.
Returns
----------
type
User-defined class whose fully-qualified name is this missing key.
Type hint whose fully-qualified name is this missing key.
Raises
----------
BeartypeCallHintForwardRefException
If either:
* This name is *not* a syntactically valid fully-qualified
classname.
* This name is *not* a syntactically valid fully-qualified Python
identifier containing at least one ``"."`` character.
* *No* module prefixed this name exists.
* An importable module prefixed by this name exists *but* this
module declares no attribute by this name.
* The module attribute to which this name refers is *not* an
isinstanceable class.
* The module attribute to which this name refers is neither:
* A subscripted generic (e.g., ``MuhGeneric[int]``).
* An isinstanceable class (e.g., ``MuhGeneric``).
'''

# Module attribute whose fully-qualified name is this forward
# reference, dynamically imported at callable call time.
hint_class: type = import_module_attr(
module_attr_name=hint_classname,
# 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,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)

# If this attribute is *NOT* an isinstanceable class, raise an
# exception.
# If this hint is a subscripted generic (e.g., ``MuhGeneric[int]``),
# reduce this hint to the class subscripting this generic (e.g., "int").
if is_hint_pep484585_generic(hint):
hint = get_hint_pep484585_generic_type(
hint=hint,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)
# Else, this hint is *NOT* a subscripted generic.

# If this hint is *NOT* an isinstanceable class, raise an exception.
die_unless_type_isinstanceable(
cls=hint_class,
cls=hint,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix=f'Forward reference "{hint_classname}" referent ',
exception_prefix=f'Forward reference {repr(hint_name)} referent ',
)
# Else, this hint is an isinstanceable class.

# Return this class. The superclass dict.__getitem__() dunder method
# then implicitly maps the passed missing key to this class by
# effectively assigning this name to this class: e.g.,
# self[hint_classname] = hint_class
return hint_class # type: ignore[return-value]
# self[hint_name] = hint
return hint # type: ignore[return-value]

# ....................{ SINGLETONS }....................
bear_typistry = Beartypistry()
bear_typistry = _Beartypistry()
'''
**Beartypistry** (i.e., singleton dictionary mapping from the fully-qualified
classnames of all type hints annotating callables decorated by the
:func:`beartype.beartype` decorator to those types).**
See Also
----------
:class:`Beartypistry`
:class:`_Beartypistry`
Further details.
'''

Expand Down
4 changes: 2 additions & 2 deletions beartype/_decor/wrap/wrapmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from beartype._check.convert.convsanify import sanify_hint_root_func
from beartype._check.forward.fwdtype import (
bear_typistry,
register_typistry_forwardref,
get_hint_forwardref_code,
)
from beartype._check.util.checkutilmake import make_func_signature
from beartype._data.func.datafuncarg import (
Expand Down Expand Up @@ -762,7 +762,7 @@ def _unmemoize_func_wrapper_code(
),
# Python expression evaluating to this class when accessed
# via the private "__beartypistry" parameter.
new=register_typistry_forwardref(
new=get_hint_forwardref_code(
# Fully-qualified classname referred to by this forward
# reference relative to the decorated callable.
get_hint_pep484585_forwardref_classname_relative_to_object(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@
@callable_cached
def is_hint_pep484585_generic(hint: object) -> bool:
'''
``True`` only if the passed object is either a :pep:`484`- or
:data:`True` only if the passed object is either a :pep:`484`- or
:pep:`585`-compliant **generic** (i.e., object that may *not* actually be a
class despite subclassing at least one PEP-compliant type hint that also
may *not* actually be a class).
Specifically, this tester returns ``True`` only if this object is either:
Specifically, this tester returns :data:`True` only if this object is
either:
* A :pep:`585`-compliant generic as tested by the lower-level
:func:`is_hint_pep585_generic` function.
Expand Down Expand Up @@ -81,11 +82,11 @@ class usually produces a generic non-class that *must* nonetheless be
Returns
----------
bool
``True`` only if this object is a generic.
:data:`True` only if this object is a generic.
See Also
----------
:func:`is_hint_pep_typevars`
:func:`beartype._util.hint.pep.utilpeptest.is_hint_pep_typevars`
Commentary on the relation between generics and parametrized hints.
'''

Expand Down Expand Up @@ -255,7 +256,7 @@ def get_hint_pep484585_generic_bases_unerased(
Raises
----------
:exc:`exception_cls`
exception_cls
If this hint is either:
* Neither a :pep:`484`- nor :pep:`585`-compliant generic.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def test_pep484_forwardref_data() -> None:
stopping_by_woods_on,
the_woods_are_lovely,
its_fields_of_snow,
winding_among_the_springs,
# between_the_woods_and_frozen_lake,
)

Expand All @@ -60,6 +61,7 @@ def test_pep484_forwardref_data() -> None:
assert the_woods_are_lovely(KNOW) is KNOW
assert its_fields_of_snow(WITH_BURNING_SMOKE) is WITH_BURNING_SMOKE[0]
assert RUGGED_AND_DARK.or_where_the_secret_caves() is RUGGED_AND_DARK
assert winding_among_the_springs(RUGGED_AND_DARK) is RUGGED_AND_DARK

#FIXME: Disabled until we decide whether we want to bother trying to
#resolve nested forward references or not.
Expand Down

0 comments on commit e82a1f7

Please sign in to comment.