Skip to content

Commit

Permalink
Triply-redeclared types. *Just don't ask.*
Browse files Browse the repository at this point in the history
This commit resolves inscrutable non-determinism (which is technically
deterministic if you squint at it, but we don't talk about that) with
respect to repeatedly redefined classes defining one or more methods
annotated by one or more **self-referential relative forward reference**
(i.e., referring to this class currently being defined), giddily
resolving issue #365 kindly submitted by recursive typonista and likely
candidate for next-life upgrade to minor deity status @iamrecursion (Ara
Adkins). Specifically, @beartype is now *considerably* more robust
against non-determinism in Jupyter cells containing
`@beartype`-decorated self-referential classes ala:

```python
from beartype import beartype

@beartype
class MuhSelfReferentialClass(object):
    def __init__(self, muh_var: int) -> None:
        self.muh_var = muh_var

    @classmethod
    def muh_factory(cls, muh_var: int) -> "MuhSelfReferentialClass":
        return MuhSelfReferentialClass(muh_var + 42)

muh_object = MuhSelfReferentialClass.muh_factory(42)
```

Flex those burly QA biceps, @beartype. Flex 'em. (*Questionable quandaries in an old n-ary manifold!*)
  • Loading branch information
leycec committed Apr 13, 2024
1 parent 75e64bc commit 1204de4
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 7 deletions.
1 change: 1 addition & 0 deletions beartype/_check/checkcache.py
Expand Up @@ -41,6 +41,7 @@ def clear_checker_caches() -> None:
:data:`beartype._check.convert.convcoerce._hint_repr_to_hint`
dictionary).
'''
print('Clearing all \"beartype._check\" caches...')

# Clear all relevant caches used throughout this subpackage.
_forwardref_to_referee.clear()
Expand Down
19 changes: 12 additions & 7 deletions beartype/_decor/_decortype.py
Expand Up @@ -395,6 +395,12 @@ def MuhClass(object): ... # <-- this makes me squint
# efficiency in the common case of module redefinition.
_BEARTYPED_MODULE_TO_TYPE_NAME.clear()

# Set of the unqualified basenames of *ALL* classes in that module
# previously decorated by this decorator, redefined *AFTER* clearing
# that set above to enable the addition of this type back to this
# new set below. Nobody ever said type-checking was gonna be easy.
type_names_beartyped = _BEARTYPED_MODULE_TO_TYPE_NAME[module_name]

# Clear *ALL* type-checking caches. Notably:
# * The forward reference referee cache (i.e., private
# "beartype._check.forward.reference.fwdrefmeta._forwardref_to_referee"
Expand All @@ -420,11 +426,10 @@ def MuhClass(object): ... # <-- this makes me squint
# wrapper functions to raise erroneous type-checking violations.
clear_checker_caches()
# Else, this is the first decoration of this class by this decorator.
# In this case...
else:
# Record that this class has now been decorated by this decorator.
# Technically, this should (probably) be performed *AFTER* this
# decorator has actually successfully decorated this class.
# Pragmatically, doing so here is simply faster and... simpler.
type_names_beartyped.add(type_name)

# Record that this class has now been decorated by this decorator.
# Technically, this should (probably) be performed *AFTER* this
# decorator has actually successfully decorated this class.
# Pragmatically, doing so here is simply faster and... simpler.
type_names_beartyped.add(type_name)
# Else, this class is *NOT* defined by a module.
45 changes: 45 additions & 0 deletions beartype_test/a00_unit/data/hint/data_hintref.py
Expand Up @@ -176,6 +176,51 @@ def or_where_the_secret_caves(self) -> 'WithSluggishSurge[T]':

return self

# ....................{ CLASSES ~ reload }....................
# Test decorating a user-defined class with the @beartype decorator where:
# 1. That class defines a method annotated by a self-referential relative
# forward reference (i.e., referring to that class currently being defined).
# 2. That method is then called.
# 3. That logic is then repeated *THREE TIMES,* thus redefining that class and
# re-calling that method three times. Doing so simulates a hot reload (i.e.,
# external reload of the hypothetical user-defined module defining that class
# and that function).
#
# For reasons that are *NOT* particularly interesting (and would consume seven
# volumes of fine print), it has to be 3 iterations. 2 is too few.
#
# See also this user-reported issue underlying this test case:
# https://github.com/beartype/beartype/issues/365
for _ in range(3):
@beartype
class OnTheBareMast(object):
'''
:func:`beartype.beartype`-decorated class defining a method annotated by
a **self-referential relative forward reference** (i.e., referring to
this class currently being defined).
'''

def __init__(self, and_took_his_lonely_seat: int) -> None:
'''
Initialize this object with the passed arbitrary parameter.
'''

self.and_took_his_lonely_seat = and_took_his_lonely_seat


@classmethod
def over_the_tranquil_sea(
cls, and_took_his_lonely_seat: int) -> 'OnTheBareMast':
'''
Method annotated by a self-referential relative forward reference.
'''

return OnTheBareMast(and_took_his_lonely_seat + 0xBABECAFE)


# Instantiate this class by invoking this factory class method.
And_felt_the_boat_speed = OnTheBareMast.over_the_tranquil_sea(0xFEEDBABE)

# ....................{ FUNCTIONS ~ pep : composite : moar }....................
# Arbitrary functions annotated by PEP-compliant forward references defined as
# non-trivial Python expressions (i.e., strings that are *NOT* reducible to
Expand Down

0 comments on commit 1204de4

Please sign in to comment.