Skip to content

Commit

Permalink
beartype.door.is_bearable() + PEP 647.
Browse files Browse the repository at this point in the history
This commit resuscitates PEP 647-compliant `typing.TypeGuard[T]`-based
type narrowing on the statement-level runtime type-checker
`beartype.door.is_bearable()`, resolving feature request #255 kindly
submitted by Florida Polytechnic polymath @alexander-c-b (Alexander C.
Bodoh) back when @leycec still had hair. Specifically, this commit:

* Restores @beartype support for PEP 647, entirely thanks to a
  [mammoth dissertation-length
  dissection](#255 (comment))
  of the intersection (*oh gods, what does any of this mean anymore) of
  runtime and static type-checking vis-a-vis the procedural
  statement-level hybrid runtime-static type-checker
  `beartype.door.is_bearable()`, the PEP 484-compliant
  `@typing.overload` decorator, *and* the PEP 647-compliant
  `typing.TypeGuard[T]` type hint – all courtesy Python's unassailable
  `typing` genius @asford (Alex Ford). A prefatory comment in the
  @beartype codebase now glibly reads:

  ```python
  # Note that this PEP 484- and 647-compliant API is entirely the brain child of
  # @asford (Alex Ford). If this breaks, redirect all ~~vengeance~~ enquiries to:
  #     https://github.com/asford
  ```

* Restores our previously disabled FAQ entry documenting @beartype
  support for PEP 647.

Sadly:

* This does *not* extend to the comparable object-oriented
  `beartype.door.TypeHint.is_bearable()` method – which continues to
  *not* perform type narrowing. Due to deficiencies in @leycec's wobbly
  brain, only the procedural `beartype.door.is_bearable()` function
  currently performs type narrowing.
* This may *not* extend to pyright. Although mypy appears to fully
  support this API, pyright appears to raise fatal errors that make *no*
  sense and suggest pyright to have an equally wobbly brain:

  ```
  /home/leycec/py/beartype/beartype/door/_doorcheck.py
    /home/leycec/py/beartype/beartype/door/_doorcheck.py:209:5 - error: Overloaded implementation is not consistent with signature of overload 1
      Function return type "TypeGuard[T@is_bearable]" is incompatible with type "bool"
        "TypeGuard[T@is_bearable]" is incompatible with "bool" (reportInconsistentOverload)
    /home/leycec/py/beartype/beartype/door/_doorcheck.py:209:5 - error: Overloaded implementation is not consistent with signature of overload 2
      Function return type "TypeGuard[T@is_bearable]" is incompatible with type "bool"
        "TypeGuard[T@is_bearable]" is incompatible with "bool" (reportInconsistentOverload)
  2 errors, 0 warnings, 0 informations
  ```

What's "funny" about that is that:

* *All* `TypeGuard[...]` type hints (including
  `TypeGuard[T@is_bearable]`, whatever that even is) reduce to and are
  thus compatible with the standard `bool` type.
* PEP 647 claims that the reference implementation of PEP 647 is
  (*...waitforit*) pyright:

      The Pyright type checker supports the behavior described in this
      PEP.

Guess it doesn't, huh? We cry wet crocodile tears for OO and pyright.
(*Wet wetlands band pet band-aids!*)
  • Loading branch information
leycec committed Mar 28, 2024
1 parent 0fccdb4 commit 874776a
Show file tree
Hide file tree
Showing 19 changed files with 276 additions and 296 deletions.
24 changes: 7 additions & 17 deletions beartype/_check/code/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,6 @@
# singletons are actually a new unique category of callable-specific type
# variables. See also:
# https://www.python.org/dev/peps/pep-0612
#* PEP 647-compliance. PEP 647 introduces a silly new subscriptable
# "typing.TypeGuard" attribute. With respect to runtime type-checking, *ALL*
# "typing.TypeGuard" subscriptions unconditionally reduce to "bool": e.g.,
# from typing import TypeGuard, Union
#
# # This...
# def muh_func(muh_param: object) -> TypeGuard[str]:
# return isinstance(muh_param, str) # <-- gods help us
#
# # This conveys the exact same runtime semantics as this.
# def muh_func(muh_param: object) -> bool:
# return isinstance(muh_param, str) # <-- gods help us
# Lastly, note that (much like "typing.NoReturn") "typing.TypeGuard"
# subscriptions are *ONLY* usable as return annotations. Raise exceptions, yo.

#FIXME: [O(n)] Ah-ha! We now know how to implement O(log n) and O(n)
#type-checking in a scaleable manner that preserves @beartype's strong
Expand Down Expand Up @@ -202,10 +188,11 @@
# __beartype_check_times = CHECK_TIMES,
# __beartype_get_time_monotonic = monotonic,
# ) -> None:
# CHECK_TIME_START = __beartype_get_time_monotonic()
#
# # Constant "J" in the inequality "Lt < J" governing @beartype's
# # deadline scheduler for non-constant type-checking, denominated in
# # fractional seconds.
# CHECK_TIME_START = __beartype_get_time_monotonic()
# CHECK_TIME_MAX = (
# {check_time_max_multiplier}(
# CHECK_TIME_START - __beartype_check_times[1]
Expand All @@ -229,9 +216,12 @@
# )
# for muh_item in list
# )
# # *AND* @beartype has yet to exceed its scheduled deadline for
# # *AND* @beartype has still yet to exceed its scheduled deadline for
# # non-constant type-checks...
# ) and ({check_time_max_multiplier - 1}*monotonic() < CHECK_TIME_MAX):
# ) and (
# {check_time_max_multiplier - 1} *
# __beartype_get_time_monotonic() < CHECK_TIME_MAX
# ):
# raise get_func_pith_violation(...)
# # Else, this pith either satisfies this hint *OR* @beartype has
# # exceeded its scheduled deadline for non-constant type-checks.
Expand Down
37 changes: 19 additions & 18 deletions beartype/_conf/confcls.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,40 +299,41 @@ def __new__(
total running time of the active Python interpreter exceeds this
integer multiplied by the running time consumed by both the current
type-check and all prior type-checks *and* the caller also passed a
non-default ``strategy``) *or* ``None`` if :mod:`beartype` should
never prematurely halt runtime type-checks.
non-default ``strategy``) *or* :data:`None` if :mod:`beartype`
should never prematurely halt runtime type-checks.
Increase this quantity to type-check more container items at a cost
of decreasing application responsiveness. Likewise, decrease this
quantity to increase application responsiveness at a cost of
type-checking fewer container items.
Increasing this integer increases the number of container items that
:mod:`beartype` type-checks at a cost of decreasing application
responsiveness. Likewise, decreasing this integer increases
application responsiveness at a cost of decreasing the number of
container items that :mod:`beartype` type-checks.
Ignored when ``strategy`` is :attr:`BeartypeStrategy.O1`, as that
strategy is already effectively instantaneous; imposing deadlines
and thus bureaucratic bookkeeping on that strategy would only
reduce its efficiency for no good reason, which is a bad reason.
Defaults to 1000, in which case a maximum of %0.1 of the total
Defaults to 1000, in which case a maximum of 0.10% of the total
runtime of the active Python process will be devoted to performing
non-constant :mod:`beartype` type-checks over container items. This
default has been carefully tuned to strike a reasonable balance
between runtime type-check coverage and application responsiveness,
typically enabling smaller containers to be fully type-checked
without noticeably impacting codebase performance.
*Theory time.* Let:
**Theory time.** Let:
* ``T`` be the total time this interpreter has been running.
* ``b`` be the total time :mod:`beartype` has spent type-checking in
this interpreter.
* :math:`T` be the total time this interpreter has been running.
* :math:``b` be the total time :mod:`beartype` has spent
type-checking in this interpreter.
Clearly, ``b <= T``. Generally, ``b <<<<<<< T`` (i.e., type-checks
consume much less time than the total time consumed by the process).
However, it's all too easy to exhibit worst-case behaviour of
``b ~= T`` (i.e., type-checks consume most of the total time). How?
By passing the :func:`beartype.door.is_bearable` tester an absurdly
large nested container subject to the non-default ``strategy`` of
:attr:`BeartypeStrategy.On`.
Clearly, :math:`b <= T`. Generally, :math:`b <<<<<<< T` (i.e.,
type-checks consume much less time than the total time consumed by
the process). However, it's all too easy to exhibit worst-case
behaviour of :math:`b ~= T` (i.e., type-checks consume most of the
total time). How? By passing the :func:`beartype.door.is_bearable`
tester an absurdly large nested container subject to the non-default
``strategy`` of :attr:`BeartypeStrategy.On`.
This deadline multiplier mitigates that worst-case behaviour.
Specifically, :mod:`beartype` will prematurely halt any iterative
Expand Down
35 changes: 28 additions & 7 deletions beartype/_data/hint/datahinttyping.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
)
from beartype._data.hint.pep.sign.datapepsigncls import HintSign
from beartype._data.func.datafuncarg import ARG_VALUE_UNPASSED
from collections.abc import Callable as CallableABC
from importlib.abc import PathEntryFinder
from pathlib import Path
from types import (
Expand All @@ -52,6 +53,33 @@
GeneratorType,
)

# ....................{ TYPEVARS }....................
S = TypeVar('S')
'''
**Unbound type variable** (i.e., matching *any* arbitrary type) locally bound to
different types than the :data:`.T` type variable.
'''


T = TypeVar('T')
'''
**Unbound type variable** (i.e., matching *any* arbitrary type) locally bound to
different types than the :data:`.S` type variable.
'''


CallableT = TypeVar('CallableT', bound=CallableABC)
'''
**Callable type variable** (i.e., bound to match *only* callables).
'''


NodeT = TypeVar('NodeT', bound=AST)
'''
**Node type variable** (i.e., type variable constrained to match *only* abstract
syntax tree (AST) nodes).
'''

# ....................{ AST }....................
NodeCallable = Union[FunctionDef, AsyncFunctionDef]
'''
Expand All @@ -70,13 +98,6 @@
'''


NodeT = TypeVar('NodeT', bound=AST)
'''
**Node type variable** (i.e., type variable constrained to match *only* abstract
syntax tree (AST) nodes).
'''


NodeVisitResult = Optional[Union[AST, List[AST]]]
'''
PEP-compliant type hint matching a **node visitation result** (i.e., object
Expand Down
56 changes: 26 additions & 30 deletions beartype/_util/cache/utilcachecall.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,18 @@

# ....................{ IMPORTS }....................
from beartype.roar._roarexc import _BeartypeUtilCallableCachedException
from beartype.typing import (
Dict,
TypeVar,
)
from beartype.typing import Dict
from beartype._data.hint.datahinttyping import CallableT
from beartype._util.func.arg.utilfuncargtest import (
die_unless_func_args_len_flexible_equal,
is_func_arg_variadic,
)
from beartype._util.text.utiltextlabel import label_callable
from beartype._util.utilobject import SENTINEL
from collections.abc import Callable
from functools import wraps

# ....................{ PRIVATE ~ hints }....................
_CallableT = TypeVar('_CallableT', bound=Callable)
'''
Type variable bound to match *only* callables.
'''

# ....................{ DECORATORS ~ callable }....................
def callable_cached(func: _CallableT) -> _CallableT:
def callable_cached(func: CallableT) -> CallableT:
'''
**Memoize** (i.e., efficiently re-raise all exceptions previously raised by
the decorated callable when passed the same parameters (i.e., parameters
Expand Down Expand Up @@ -82,7 +73,7 @@ def callable_cached(func: _CallableT) -> _CallableT:
#. Returns that value.
Caveats
----------
-------
**The decorated callable must accept no keyword parameters.** While this
decorator previously memoized keyword parameters, doing so incurred
significant performance penalties defeating the purpose of caching. This
Expand Down Expand Up @@ -144,16 +135,16 @@ class and returns a boolean. Under conservative assumptions of 4 bytes of
Parameters
----------
func : _CallableT
func : CallableT
Callable to be memoized.
Returns
----------
_CallableT
-------
CallableT
Closure wrapping this callable with memoization.
Raises
----------
------
_BeartypeUtilCallableCachedException
If this callable accepts a variadic positional parameter (e.g.,
``*args``).
Expand Down Expand Up @@ -187,7 +178,7 @@ def _callable_cached(*args):
Memoized variant of the {func.__name__}() callable.
See Also
----------
--------
:func:`callable_cached`
Further details.
'''
Expand Down Expand Up @@ -273,7 +264,7 @@ def _callable_cached(*args):
return _callable_cached # type: ignore[return-value]

# ....................{ DECORATORS ~ method }....................
def method_cached_arg_by_id(func: _CallableT) -> _CallableT:
def method_cached_arg_by_id(func: CallableT) -> CallableT:
'''
**Memoize** (i.e., efficiently re-raise all exceptions previously raised by
the decorated method when passed the same *exact* parameters (i.e.,
Expand All @@ -282,7 +273,7 @@ def method_cached_arg_by_id(func: _CallableT) -> _CallableT:
rather than inefficiently recalling that method) the passed method.
Caveats
----------
-------
**This decorator is only intended to decorate bound methods** (i.e., either
class or instance methods bound to a class or instance). This decorator is
*not* intended to decorate functions or static methods.
Expand Down Expand Up @@ -333,16 +324,16 @@ def _is_equal(self, other: 'MuhClass') -> bool:
Parameters
----------
func : _CallableT
func : CallableT
Callable to be memoized.
Returns
----------
_CallableT
-------
CallableT
Closure wrapping this callable with memoization.
Raises
----------
------
_BeartypeUtilCallableCachedException
If this callable accepts either:
Expand All @@ -351,7 +342,7 @@ def _is_equal(self, other: 'MuhClass') -> bool:
* A variadic positional parameter (e.g., ``*args``).
See Also
----------
--------
:func:`callable_cached`
Further details.
'''
Expand Down Expand Up @@ -410,7 +401,7 @@ def _method_cached(self_or_cls, arg):
Memoized variant of the {func.__name__}() callable.
See Also
----------
--------
:func:`callable_cached`
Further details.
'''
Expand Down Expand Up @@ -486,7 +477,7 @@ def _method_cached(self_or_cls, arg):
return _method_cached # type: ignore[return-value]

# ....................{ DECORATORS ~ property }....................
def property_cached(func: _CallableT) -> _CallableT:
def property_cached(func: CallableT) -> CallableT:
'''
**Memoize** (i.e., efficiently cache and return all previously returned
values of the passed property method as well as all previously raised
Expand All @@ -507,7 +498,7 @@ def property_cached(func: _CallableT) -> _CallableT:
called at most once for each object exposing this property.
Caveats
----------
-------
**This decorator must be preceded by an explicit usage of the standard**
:class:`property` **decorator.** Although this decorator could be trivially
refactored to automatically decorate the returned property method by the
Expand Down Expand Up @@ -545,8 +536,13 @@ def property_cached(func: _CallableT) -> _CallableT:
Parameters
----------
func : _CallableT
func : CallableT
Property method to be memoized.
Returns
-------
CallableT
Dynamically generated function wrapping this property with memoization.
'''
assert callable(func), f'{repr(func)} not callable.'

Expand Down Expand Up @@ -644,7 +640,7 @@ def property_method_cached(self, __property_method=__property_method):
function definition time as the default value of an arbitrary parameter.
Design
----------
------
While there exist numerous alternative implementations for caching properties,
the approach implemented below has been profiled to be the most efficient.
Alternatives include (in order of decreasing efficiency):
Expand Down
17 changes: 3 additions & 14 deletions beartype/_util/cache/utilcachemeta.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,10 @@
'''

# ....................{ IMPORTS }....................
from beartype.typing import (
Dict,
Optional,
Tuple,
Type,
TypeVar,
)
from beartype.typing import Type
from beartype._data.hint.datahinttyping import T
from beartype._util.cache.utilcachecall import callable_cached

# ....................{ PRIVATE ~ hints }....................
_T = TypeVar('_T')
'''
PEP-compliant type variable matching any arbitrary object.
'''

# ....................{ METACLASSES }....................
class BeartypeCachingMeta(type):
'''
Expand Down Expand Up @@ -64,7 +53,7 @@ class BeartypeCachingMeta(type):

# ..................{ INITIALIZERS }..................
@callable_cached
def __call__(cls: Type[_T], *args) -> _T: # type: ignore[reportIncompatibleMethodOverride]
def __call__(cls: Type[T], *args) -> T: # type: ignore[reportIncompatibleMethodOverride]
'''
Instantiate the passed class with the passed positional arguments if
this is the first instantiation of this class passed these arguments
Expand Down

0 comments on commit 874776a

Please sign in to comment.