Skip to content

Commit

Permalink
Postponed evaluation of non-referential hints x 6.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain generalizing @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 **arbitrary forward references** (i.e.,
references to user-defined *objects* that have yet to be defined in the
current lexical scope, regardless of whether those objects are types or
not), en-route to resolving feature request #226 kindly submitted by
Oxford Aspiring Algorithms Aficionado (AAA) @eohomegrownapps (Euan Ong).
Specifically, this commit:

* Generalizes our recently added `beartype._decor.forward._fwdref`
  submodule to support arbitrary `isinstance()` resolution -- including
  *eventual* resolution of recursive type hints (which necessarily
  employ forward references to convey the recursion).
* Shifts the prior `beartype._decor.cache.cachedecor` submodule to
  `beartype._decor.decorcache` for orthogonality.
* Removes the now-empty `beartype._decor.cache` subpackage.

(*Not a peep from deep peeps!*)
  • Loading branch information
leycec committed Aug 18, 2023
1 parent 0d2da37 commit 2807caf
Show file tree
Hide file tree
Showing 9 changed files with 94 additions and 33 deletions.
2 changes: 1 addition & 1 deletion beartype/_check/code/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1672,7 +1672,7 @@
#* In that submodule:
# * Rename the existing @beartype decorator to beartype_template(). That
# function will now only be called internally rather than externally.
#* Define a new private "beartype._decor.cache.cachedecor" submodule.
#* Define a new private "beartype._decor.decorcache" submodule.
#* In that submodule:
# * Define a new "BEARTYPE_PARAMS_TO_DECOR" dictionary mapping from a *TUPLE*
# of positional arguments listed in the exact same order as the optional
Expand Down
Empty file removed beartype/_decor/cache/__init__.py
Empty file.
File renamed without changes.
2 changes: 1 addition & 1 deletion beartype/_decor/decorcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'''
**Unmemoized beartype decorators** (i.e., core lower-level unmemoized decorators
underlying the higher-level memoized :func:`beartype.beartype` decorator, whose
implementation in the parent :mod:`beartype._decor.cache.cachedecor` submodule
implementation in the parent :mod:`beartype._decor.decorcache` submodule
is a thin wrapper efficiently memoizing closures internally created and returned
by that decorator; in turn, those closures directly defer to this submodule).
Expand Down
2 changes: 1 addition & 1 deletion beartype/_decor/decormain.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def beartype( # type: ignore[no-redef]
# case, define the @beartype decorator in the standard way.
else:
# This is where @beartype *REALLY* lives. Grep here for all the goods.
from beartype._decor.cache.cachedecor import beartype
from beartype._decor.decorcache import beartype

# ....................{ DECORATORS ~ doc }....................
# Document the @beartype decorator with the same documentation regardless of
Expand Down
56 changes: 32 additions & 24 deletions beartype/_decor/forward/_fwdref.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,6 @@
via non-trivial call stack inspection.
'''

# ....................{ TODO }....................
#FIXME: Honestly, does this submodule actually need to exist, really? Sure, we
#went to a great deal of effort to implement all of this -- but it kinda all
#seems for naught, now. After all, shouldn't we simply generalize the "fwdtype"
#submodule to handle these edge cases -- or has the "Beartypistry" generally
#outlived its usefulness? Should we instead obsolete the "fwdtype" submodule in
#favour of this submodule? I honestly have no idea.
#
#Consider recursive type hints, which leverage relative forward references. It
#seems likely that *THIS* submodule rather than the "fwdtype" submodule will be
#required to resolve recursive type hints. Notably, the
#_BeartypeForwardRefMeta.__instancecheck__() method can be generalized to at
#least detect recursion... maybe? Or should that detection instead be handled
#in the higher-level "fwdscope" submodule? *UGH*! So hard. Y dis so hard. *sigh*

# ....................{ IMPORTS }....................
from beartype.roar import BeartypeDecorHintForwardRefException
from beartype.typing import (
Expand Down Expand Up @@ -116,14 +101,9 @@ class referenced by the passed **forward reference subclass** (i.e.,
class referenced by this forward reference subclass.
'''

# Return true only if this object is an instance of the external class
# referenced by this forward reference.
#
# Note that this is "good enough" for now. Our existing "bear_typistry"
# dictionary already handles lookup-based resolution and caching of
# forward references at runtime; so, just defer to that for now as the
# trivial solution. Next!
return isinstance(obj, bear_typistry[cls.attr_name])
# Return true only if this forward reference subclass insists that this
# object satisfies the external class referenced by this subclass.
return cls.is_instance(obj)

# ....................{ SUPERCLASSES }....................
#FIXME: Unit test us up, please.
Expand Down Expand Up @@ -160,12 +140,40 @@ def __new__(cls, *args, **kwargs) -> NoReturn:
Prohibit instantiation by unconditionally raising an exception.
'''

#
# Instantiatable. It's a word or my username isn't @UncleBobOnAStick.
raise BeartypeDecorHintForwardRefException(
f'{repr(_BeartypeForwardRefABC)} subclass '
f'{repr(cls)} not instantiatable.'
)

# ....................{ TESTERS }....................
@classmethod
def is_instance(cls, obj: object) -> bool:
'''
:data:`True` only if the passed object is an instance of the external
class referred to by this forward reference.
Parameters
----------
obj : object
Arbitrary object to be tested.
Returns
----------
bool
:data:`True` only if this object is an instance of the external
class referred to by this forward reference subclass.
'''

# Return true only if this object is an instance of the external class
# referenced by this forward reference.
#
# Note that this is "good enough" for now. Our existing "bear_typistry"
# dictionary already handles lookup-based resolution and caching of
# forward references at runtime; so, just defer to that for now as the
# trivial solution. Next!
return isinstance(obj, bear_typistry[cls.attr_name])

# ....................{ FACTORIES }....................
#FIXME: Unit test us up, please.
def make_forwardref_subtype(attr_name: str) -> Type[_BeartypeForwardRefABC]:
Expand Down
61 changes: 57 additions & 4 deletions beartype/_decor/forward/fwdscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
# RecursiveTypeHint = List['RecursiveTypeHint']
# NonrecursiveTypeHint = list
#
# @beartype(arg_to_type_hint_names={
# @beartype(_arg_to_type_hint_names={
# 'muh_first_arg': 'RecursiveTypeHint',
# 'muh_other_arg': 'NonrecursiveTypeHint',
# })
Expand All @@ -44,13 +44,66 @@
# muh_other_arg: NonrecursiveTypeHint,
# muh_final_arg: list[int], # <-- *NOT A PYTHON IDENTIFIER*
# ) -> None: ...
#
# Note that our prospective "_arg_to_type_hint_names" parameter is
# intentionally privatized.
#
# Feasible? Certainly. Non-trivial? Certainly. *shrug*
# * Additionally, note that even the above doesn't suffice. Yes, it *DOES*
# suffice for the trivial case of a recursive type hint passed as a root type
# hint. But what about the non-trivial case of a recursive type hint embedded
# in a parent type hint (e.g., "set[RecursiveTypeHint]")? To properly handle
# this, we would need to... do what exactly? We have *NO* idea. Is this even
# worth doing if we can't reasonably detect embedded recursive type hints?
# *sigh*
# * It might make more sense to attempt to:
# * Detect attribute assignments that appear to be defining recursive type
# hints. Ugh. The issue here is that we could conceivably do so for the
# current submodule but *NOT* across module boundaries (without caching an
# insane amount of on-disk metadata, anyway).
# * Pass a tuple of the names of all such attributes to @beartype.
# Okay. So, we're agreed we are *NOT* doing that. NO ON-DISK CACHING... yet.
# * Perhaps we can, indeed, detect embedded recursive type hints by:
# * Parsing apart something like "set[RecursiveTypeHint]" into the frozen set
# of all Python identifiers referenced by that type hint. In this case,
# that would be "frozenset(('set', 'RecursiveTypeHint'))". Of course, we
# could further optimize this by excluding builtin names (e.g., "set").
# Seems reasonable... maybe? Presumably, this requires recursively visiting
# each root type hint AST node to iteratively construct this frozen set.
# * *OH. OH, BOY.* I sorta figured out how to detect embedded recursive type
# hints -- but it's insane. Just passing frozen sets or whatever doesn't
# work, because that approach invites obvious false positives in various edge
# cases. The *ONLY* approach that is robust, deterministic, and failure-proof
# against all edge cases is to pass *THE AST NODE ENCAPSULATING EACH TYPE
# HINT* to @beartype. We pass AST nodes; not the names of type hints: e.g.,
# from typing import List
# RecursiveTypeHint = List['RecursiveTypeHint']
# NonrecursiveTypeHint = list
#
# @beartype(_arg_to_type_hint_node={
# 'muh_first_arg': AstNode(name='set[RecursiveTypeHint]'), # <-- fake AST node, obviously
# 'muh_other_arg': AstNode(name='NonrecursiveTypeHint'), # <-- more faking just for show
# })
# def muh_func(
# muh_first_arg: set[RecursiveTypeHint],
# muh_other_arg: NonrecursiveTypeHint,
# ) -> None: ...
#
# Fairly convinced that would work. The idea here is that our code generation
# algorithm would iteratively visit each child AST node in parallel to the
# actual child type hint that it is currently visiting. Doing so enables
# @beartype to then decide whether a type hint is recursive or not... in
# theory, anyway. Note, however, that this is still *EXTREMELY* non-trivial.
# Like, our code generation algorithm would probably need to maintain an
# internal stack of the names of all parent type hints on the current path to
# the currently visited child type hint. lolwut?
# * Raise a human-readable exception when detecting a recursive type hint. This
# is better than raising a non-human-readable exception, which we currently
# do. Naturally, eventually, we should instead...
# * Dynamically generate a new recursive type-checking tester function that
# recursively calls itself indefinitely. If doing so generates a
# "RecursionError", @beartype considers that the user's problem. *wink*
# * Dynamically generate a new recursive type-checking
# BeartypeForwardRef_{attr_name}.is_instance() tester method that recursively
# calls itself indefinitely. If doing so generates a "RecursionError",
# @beartype considers that the user's problem. *wink*
#
#Let's revisit this when we've at least finalized the initial implementation of
#"BeartypeForwardScope", please.
Expand Down
2 changes: 1 addition & 1 deletion beartype/claw/_clawmagic.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@
'''

# ....................{ STRINGS ~ decorator }....................
BEARTYPE_DECORATOR_MODULE_NAME = 'beartype._decor.cache.cachedecor'
BEARTYPE_DECORATOR_MODULE_NAME = 'beartype._decor.decorcache'
'''
Fully-qualified name of the submodule defining the **beartype decorator** (i.e.,
:mod:`beartype` decorator applied by our abstract syntax tree (AST) node
Expand Down
2 changes: 1 addition & 1 deletion beartype/door/_doorcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
BeartypeConf,
)
# from beartype._data.hint.datahintfactory import TypeGuard
from beartype._decor.cache.cachedecor import beartype
from beartype._decor.decorcache import beartype
from beartype._util.cache.utilcachecall import callable_cached
from beartype._util.error.utilerror import reraise_exception_placeholder
from beartype._util.hint.utilhinttest import die_unless_hint
Expand Down

0 comments on commit 2807caf

Please sign in to comment.