Skip to content

Commit

Permalink
PEP 563 + PEP 604 union + undefined types.
Browse files Browse the repository at this point in the history
This commit generalizes @beartype to support an edge case with respect
to PEPs 557, 563, and 604, resolving issue #342 kindly submitted by
yeasty VimScript bioinformatics guru @brettc (Brett Calcott).
Specifically, this commit resolves an issue arising from which enabling
PEP 563 via from `__future__ import annotations`, defining a PEP 604
union over multiple undefined types, and then annotating a PEP 557
`@dataclass` with that union. We *did* say edge case, didn't we?
Unsurprisingly, this was surprisingly non-trivial. This is why you just
lets @beartype do the heavy lifting. (*Heavy levy!*)
  • Loading branch information
leycec committed Mar 19, 2024
1 parent a342229 commit 8e3cef6
Show file tree
Hide file tree
Showing 14 changed files with 554 additions and 243 deletions.
31 changes: 30 additions & 1 deletion beartype/_cave/_cavefast.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,13 @@
import functools as _functools
import numbers as _numbers
import re as _re
import types as _types
import typing as _typing
from beartype.roar import BeartypeCallUnavailableTypeException
from beartype._cave._caveabc import BoolType
from beartype._util.py.utilpyversion import (
IS_PYTHON_AT_LEAST_3_12,
IS_PYTHON_AT_LEAST_3_10,
IS_PYTHON_AT_LEAST_3_9,
)
from collections import deque as _deque
Expand Down Expand Up @@ -960,6 +962,33 @@ class or instance attributes).
'''

# ....................{ TYPES ~ hint : pep : 604 }....................
# If this submodule is currently being statically type-checked by a pure static
# type-checker, ignore false positives complaining that this type is not a type.
if TYPE_CHECKING:
class HintPep604Type(object): pass
# Else, this submodule is *NOT* currently being statically type-checked by a
# pure static type-checker. In this case, define this type properly. *sigh*
else:
# Define this type as either...
HintPep604Type = (
# If the active Python interpreter targets at least Python >= 3.10 and
# thus supports PEP 604, this type;
_types.UnionType
if IS_PYTHON_AT_LEAST_3_10 else
# Else, a placeholder type.
UnavailableType
)
'''
C-based type of all :pep:`604`-compliant **new unions** (i.e., objects
created by expressions of the form ``{type1} | {type2} | ... | {typeN}``) if
the active Python interpreter targets Python >= 3.10 *or*
:class:`.UnavailableType` otherwise.
This type is a version-agnostic generalization of the standard
:class:`types.UnionType` type available only under Python >= 3.10.
'''


HintPep604Types: _TupleTyping[type, ...] = (type, HintGenericSubscriptedType)
'''
Tuple of all :pep:`604`-compliant **new union item types** (i.e., types of all
Expand Down Expand Up @@ -987,7 +1016,7 @@ class HintPep695Type(object): pass
# pure static type-checker. In this case, define this type properly. *sigh*
else:
# Define this type as either...
HintPep695Type: type = (
HintPep695Type = (
# If the active Python interpreter targets at least Python >= 3.12 and
# thus supports PEP 695, this type;
_typing.TypeAliasType
Expand Down
6 changes: 3 additions & 3 deletions beartype/_check/code/codescope.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
)
from beartype._util.cls.pep.utilpep3119 import (
die_unless_type_isinstanceable,
die_unless_type_or_types_isinstanceable,
die_unless_object_isinstanceable,
)
from beartype._util.cls.utilclstest import is_type_builtin
from beartype._util.func.utilfuncscope import add_func_scope_attr
Expand Down Expand Up @@ -428,8 +428,8 @@ def add_func_scope_types(

# If either this container is *NOT* a tuple or is a tuple containing one or
# more items that are *NOT* isinstanceable classes, raise an exception.
die_unless_type_or_types_isinstanceable(
type_or_types=types, exception_prefix=exception_prefix)
die_unless_object_isinstanceable(
obj=types, exception_prefix=exception_prefix)
# Else, this container is a tuple containing only isinstanceable classes.

# If this container is a tuple *AND* the caller failed to guarantee this
Expand Down
7 changes: 4 additions & 3 deletions beartype/_check/forward/reference/fwdrefmeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from beartype.roar import BeartypeCallHintForwardRefException
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.cls.pep.utilpep3119 import (
die_unless_object_isinstanceable)
from beartype._util.hint.pep.proposal.pep484585.utilpep484585generic import (
is_hint_pep484585_generic,
get_hint_pep484585_generic_type,
Expand Down Expand Up @@ -306,8 +307,8 @@ def __type_beartype__(cls: BeartypeForwardRef) -> type: # type: ignore[misc]

# If this referee is *NOT* an isinstanceable class, raise an
# exception.
die_unless_type_isinstanceable(
cls=referee,
die_unless_object_isinstanceable(
obj=referee,
exception_cls=BeartypeCallHintForwardRefException,
exception_prefix='Forward reference ',
)
Expand Down
21 changes: 9 additions & 12 deletions beartype/_decor/_decornontype.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def beartype_nontype(obj: BeartypeableT, **kwargs) -> BeartypeableT:
defined by this submodule (e.g., :func:`.beartype_func`).
Returns
----------
-------
BeartypeableT
New pure-Python callable wrapping this beartypeable with type-checking.
'''
Expand All @@ -86,7 +86,6 @@ def beartype_nontype(obj: BeartypeableT, **kwargs) -> BeartypeableT:
# resolve to warrant doing so. To do so, this conditional branch effectively
# reorders @beartype to be the first decorator decorating the pure-Python
# function underlying this method descriptor: e.g.,
#
# # This branch detects and reorders this edge case...
# class MuhClass(object):
# @beartype
Expand Down Expand Up @@ -123,7 +122,7 @@ def beartype_nontype(obj: BeartypeableT, **kwargs) -> BeartypeableT:
# runtime type-checking into this pure-Python callable by replacing the
# bound method descriptor of the type of this object implementing the
# __call__() dunder method with a comparable descriptor calling a
# @beartype-generated runtime type-checking wrapper function.
# @beartype-generated runtime type-checking wrapper function. Go with it.
elif not is_func_python(obj):
return beartype_pseudofunc(obj, **kwargs) # type: ignore[return-value]
# Else, this object is a pure-Python function.
Expand All @@ -144,7 +143,6 @@ def beartype_nontype(obj: BeartypeableT, **kwargs) -> BeartypeableT:
# type-checkers dynamically generated by @beartype for those managers would
# erroneously raise type-checking violations after calling those managers
# and detecting the apparent type violation: e.g.,
#
# >>> from beartype.typing import Iterator
# >>> from contextlib import contextmanager
# >>> @contextmanager
Expand All @@ -155,7 +153,6 @@ def beartype_nontype(obj: BeartypeableT, **kwargs) -> BeartypeableT:
# This conditional branch effectively reorders @beartype to be the first
# decorator decorating the callable underlying this context manager,
# preserving consistency between return types *AND* return type hints: e.g.,
#
# from beartype.typing import Iterator
# from contextlib import contextmanager
#
Expand Down Expand Up @@ -200,7 +197,7 @@ def beartype_func(
:meth:`beartype._check.checkcall.BeartypeCall.reinit` method.
Returns
----------
-------
BeartypeableT
New pure-Python callable wrapping this callable with type-checking.
'''
Expand Down Expand Up @@ -316,7 +313,7 @@ def beartype_func_contextlib_contextmanager(
decorator on the pure-Python function encapsulated in this descriptor.
Returns
----------
-------
BeartypeableT
New pure-Python callable wrapping this context manager with
type-checking.
Expand Down Expand Up @@ -354,12 +351,12 @@ def beartype_descriptor_decorator_builtin(
decorator on the pure-Python function encapsulated in this descriptor.
Returns
----------
-------
BeartypeableT
New pure-Python callable wrapping this descriptor with type-checking.
Raises
----------
------
BeartypeDecorWrappeeException
If this descriptor is neither a class, property, or static method
descriptor.
Expand Down Expand Up @@ -499,7 +496,7 @@ class method defined by the class being instantiated) with dynamically
decorator on the pure-Python function encapsulated in this descriptor.
Returns
----------
-------
BeartypeableT
New pure-Python callable wrapping this descriptor with type-checking.
'''
Expand Down Expand Up @@ -581,7 +578,7 @@ def beartype_pseudofunc(pseudofunc: BeartypeableT, **kwargs) -> BeartypeableT:
decorator on the pure-Python function encapsulated in this descriptor.
Returns
----------
-------
BeartypeableT
The object monkey-patched by :func:`beartype.beartype`.
'''
Expand Down Expand Up @@ -689,7 +686,7 @@ def beartype_pseudofunc_functools_lru_cache(
decorator on the pure-Python function encapsulated in this descriptor.
Returns
----------
-------
BeartypeableT
New pseudo-callable monkey-patched by :func:`beartype.beartype`.
'''
Expand Down

0 comments on commit 8e3cef6

Please sign in to comment.