Skip to content

Commit

Permalink
Isomorphic non-closure wrapper support.
Browse files Browse the repository at this point in the history
This commit generalizes the `@beartype` decorator to support *all*
**isomorphic wrappers** (i.e., higher-level callables decorated by the
standard `@functools.wraps` decorator for wrapping lower-level callables
with additional functionality defined by even higher-level decorators
such that those wrappers isomorphically preserve both the number and
types of all passed parameters and returns by accepting only a variadic
positional argument and a variadic keyword argument), partially
resolving issue #295 kindly submitted by @patrick-kidger (Patrick
Kidger) – the Best Google X Researcher of All Time, Clearly. Previously,
@beartype only supported **isomorphic closure wrappers** (i.e.,
isomorphic wrappers defined as closures rather than non-closure
callables). Now, @beartype supports both isomorphic closure wrappers
*and* **isomorphic non-closure wrappers** (i.e., isomorphic wrappers
defined as non-closure callables rather than closures). Notably, this is
now fine:

```python
from beartype import beartype
from functools import wraps
import inspect

def muh_func(muh_arg: int):  # <-- wat!? no @beartype? how can this be?
    pass

@beartype  # <-- oh, okay. here's the @beartype. phew. that was close
@wraps(f)  # <-- standard decorator idiom
def muh_wrapper(*args, **kwargs):  # <-- isomorphic non-closure wrapper
    pass
```

(*Idiomatically immaterial idiocy in a cyclic automata, mate!*)
  • Loading branch information
leycec committed Oct 24, 2023
1 parent aa6bea8 commit 973a00e
Show file tree
Hide file tree
Showing 12 changed files with 246 additions and 146 deletions.
4 changes: 2 additions & 2 deletions beartype/_check/checkcall.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
is_func_coro,
is_func_nested,
)
from beartype._util.func.utilfuncwrap import unwrap_func_all_closures_isomorphic
from beartype._util.func.utilfuncwrap import unwrap_func_all_isomorphic

# ....................{ CLASSES }....................
class BeartypeCall(object):
Expand Down Expand Up @@ -365,7 +365,7 @@ def reinit(
self.func_wrappee = func

# Possibly unwrapped callable unwrapped from this wrappee callable.
self.func_wrappee_wrappee = unwrap_func_all_closures_isomorphic(func)
self.func_wrappee_wrappee = unwrap_func_all_isomorphic(func)
# self.func_wrappee_wrappee = unwrap_func_all(func)
# print(f'func_wrappee: {self.func_wrappee}')
# print(f'func_wrappee_wrappee: {self.func_wrappee_wrappee}')
Expand Down
28 changes: 14 additions & 14 deletions beartype/_util/func/arg/utilfuncargget.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,14 @@ def get_func_arg_first_name_or_none(
Pure-Python callable, frame, or code object to be inspected.
is_unwrap: bool, optional
:data:`True` only if this getter implicitly calls the
:func:`.unwrap_func_all_closures_isomorphic` function. Defaults to :data:`True` for safety. See
:func:`.unwrap_func_all_isomorphic` function. Defaults to :data:`True` for safety. See
:func:`.iter_func_args` for further commentary.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilCallableException`.
Returns
----------
-------
Optional[str]
Either:
Expand All @@ -62,8 +62,8 @@ def get_func_arg_first_name_or_none(
* Else, :data:`None`.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If that callable is *not* pure-Python.
'''

Expand Down Expand Up @@ -102,20 +102,20 @@ def get_func_args_flexible_len(
Pure-Python callable, frame, or code object to be inspected.
is_unwrap: bool, optional
:data:`True` only if this getter implicitly calls the
:func:`.unwrap_func_all_closures_isomorphic` function. Defaults to :data:`True` for safety. See
:func:`.iter_func_args` for further commentary.
:func:`.unwrap_func_all_isomorphic` function. Defaults to :data:`True`
for safety. See :func:`.iter_func_args` for further commentary.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilCallableException`.
Returns
----------
-------
int
Number of flexible parameters accepted by this callable.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If that callable is *not* pure-Python.
'''

Expand Down Expand Up @@ -150,20 +150,20 @@ def get_func_args_nonvariadic_len(
Pure-Python callable, frame, or code object to be inspected.
is_unwrap: bool, optional
:data:`True` only if this getter implicitly calls the
:func:`.unwrap_func_all_closures_isomorphic` function. Defaults to :data:`True` for safety. See
:func:`.iter_func_args` for further commentary.
:func:`.unwrap_func_all_isomorphic` function. Defaults to :data:`True`
for safety. See :func:`.iter_func_args` for further commentary.
exception_cls : type, optional
Type of exception to be raised in the event of a fatal error. Defaults
to :class:`._BeartypeUtilCallableException`.
Returns
----------
-------
int
Number of flexible parameters accepted by this callable.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If that callable is *not* pure-Python.
'''

Expand Down
14 changes: 7 additions & 7 deletions beartype/_util/func/arg/utilfuncargiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)
from beartype._data.kind.datakinddict import DICT_EMPTY
from beartype._util.func.utilfunccodeobj import get_func_codeobj
from beartype._util.func.utilfuncwrap import unwrap_func_all_closures_isomorphic
from beartype._util.func.utilfuncwrap import unwrap_func_all_isomorphic
from collections.abc import Callable
from enum import (
Enum,
Expand Down Expand Up @@ -198,7 +198,7 @@ def iter_func_args(
:data:`ArgMandatory`).
Caveats
----------
-------
**This highly optimized generator function should always be called in lieu
of the highly unoptimized** :func:`inspect.signature` **function,** which
implements a similar introspection as this generator with significantly
Expand All @@ -214,7 +214,7 @@ def iter_func_args(
comparatively slower :func:`get_func_codeobj` function.
is_unwrap: bool, optional
:data:`True` only if this generator implicitly calls the
:func:`unwrap_func_all_closures_isomorphic` function to unwrap this possibly higher-level
:func:`unwrap_func_all_isomorphic` function to unwrap this possibly higher-level
wrapper into its possibly lowest-level wrappee *before* returning the
code object of that wrappee. Note that doing so incurs worst-case time
complexity ``O(n)`` for ``n`` the number of lower-level wrappees
Expand All @@ -233,21 +233,21 @@ def iter_func_args(
to :class:`._BeartypeUtilCallableException`.
Yields
----------
------
ArgMeta
Parameter metadata tuple describing the currently yielded parameter.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If that callable is *not* pure-Python.
'''

# ..................{ LOCALS ~ noop }..................
# If unwrapping that callable, do so *BEFORE* obtaining the code object of
# that callable for safety (to avoid desynchronization between the two).
if is_unwrap:
func = unwrap_func_all_closures_isomorphic(func)
func = unwrap_func_all_isomorphic(func)
# Else, that callable is assumed to have already been unwrapped by the
# caller. We should probably assert that, but doing so requires an
# expensive call to hasattr(). What you gonna do?
Expand Down
34 changes: 17 additions & 17 deletions beartype/_util/func/arg/utilfuncargtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def die_unless_func_args_len_flexible_equal(
Number of flexible parameters to validate this callable as accepting.
is_unwrap: bool, optional
``True`` only if this validator implicitly calls the
:func:`unwrap_func_all_closures_isomorphic` function to unwrap this possibly higher-level
:func:`unwrap_func_all_isomorphic` function to unwrap this possibly higher-level
wrapper into its possibly lowest-level wrappee *before* returning the
code object of that wrappee. Note that doing so incurs worst-case time
complexity ``O(n)`` for ``n`` the number of lower-level wrappees
Expand All @@ -71,8 +71,8 @@ def die_unless_func_args_len_flexible_equal(
:class:`_BeartypeUtilCallableException`.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If this callable either:
* Is *not* callable.
Expand Down Expand Up @@ -173,13 +173,13 @@ def is_func_argless(
:class:`_BeartypeUtilCallableException`.
Returns
----------
-------
bool
:data:`True` only if the passed callable accepts *no* arguments.
Raises
----------
:exc:`exception_cls`
------
exception_cls
If the passed callable is *not* pure-Python.
'''

Expand Down Expand Up @@ -209,12 +209,12 @@ def is_func_arg_nonvariadic(func: Codeobjable) -> bool:
Pure-Python callable, frame, or code object to be inspected.
Returns
----------
-------
bool
:data:`True` only if that callable accepts any non-variadic parameters.
Raises
----------
------
_BeartypeUtilCallableException
If that callable is *not* pure-Python.
'''
Expand All @@ -239,15 +239,15 @@ def is_func_arg_variadic(func: Codeobjable) -> bool:
Pure-Python callable, frame, or code object to be inspected.
Returns
----------
-------
bool
:data:`True` only if that callable accepts either:
* Variadic positional arguments (e.g., ``*args``).
* Variadic keyword arguments (e.g., ``**kwargs``).
Raises
----------
------
_BeartypeUtilCallableException
If that callable is *not* pure-Python.
'''
Expand All @@ -274,13 +274,13 @@ def is_func_arg_variadic_positional(func: Codeobjable) -> bool:
Pure-Python callable, frame, or code object to be inspected.
Returns
----------
-------
bool
:data:`True` only if the passed callable accepts a variadic positional
argument.
Raises
----------
------
_BeartypeUtilCallableException
If the passed callable is *not* pure-Python.
'''
Expand All @@ -307,13 +307,13 @@ def is_func_arg_variadic_keyword(func: Codeobjable) -> bool:
Pure-Python callable, frame, or code object to be inspected.
Returns
----------
-------
bool
:data:`True` only if the passed callable accepts a variadic keyword
argument.
Raises
----------
------
_BeartypeUtilCallableException
If the passed callable is *not* pure-Python.
'''
Expand Down Expand Up @@ -346,7 +346,7 @@ def is_func_arg_name(func: Callable, arg_name: str) -> bool:
with the passed name.
Caveats
----------
-------
**This tester exhibits worst-case time complexity** ``O(n)`` **for** ``n``
**the total number of arguments accepted by this callable,** due to
unavoidably performing a linear search for an argument with this name is
Expand All @@ -361,12 +361,12 @@ def is_func_arg_name(func: Callable, arg_name: str) -> bool:
Name of the argument to be searched for.
Returns
----------
-------
bool
:data:`True` only if that callable accepts an argument with this name.
Raises
----------
------
_BeartypeUtilCallableException
If the passed callable is *not* pure-Python.
'''
Expand Down
2 changes: 1 addition & 1 deletion beartype/_util/func/mod/utilfuncmodtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def is_func_contextlib_contextmanager(func: Any) -> TypeGuard[Callable]:
# @contextlib.contextmanager decorator.
#
# Note that we *COULD* technically also explicitly test whether that
# callable satisfies the is_func_closure_isomorphic() tester, but that
# callable satisfies the is_func_wrapper_isomorphic() tester, but that
# there's no benefit and a minor efficiency cost to doing so.
return func_codeobj_name == CONTEXTLIB_CONTEXTMANAGER_CODEOBJ_NAME

Expand Down
4 changes: 2 additions & 2 deletions beartype/_util/func/utilfunccode.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,11 +657,11 @@ def get_func_code_or_none(
# if isinstance(func, CallableTypes):
# # Avoid circular import dependencies.
# from beartype._util.func.utilfuncfile import get_func_filename_or_none
# from beartype._util.func.utilfuncwrap import unwrap_func_all_closures_isomorphic
# from beartype._util.func.utilfuncwrap import unwrap_func_all_isomorphic
#
# # Code object underlying the passed pure-Python callable unwrapped if
# # this callable is pure-Python *OR* "None" otherwise.
# func_filename = get_func_filename_or_none(unwrap_func_all_closures_isomorphic(func))
# func_filename = get_func_filename_or_none(unwrap_func_all_isomorphic(func))
#
# # If this callable has a code object, set this label to either the
# # absolute filename of the physical Python module or script declaring
Expand Down
4 changes: 2 additions & 2 deletions beartype/_util/func/utilfuncscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ def get_func_globals(

# Avoid circular import dependencies.
from beartype._util.func.utilfunctest import die_unless_func_python
from beartype._util.func.utilfuncwrap import unwrap_func_all_closures_isomorphic
from beartype._util.func.utilfuncwrap import unwrap_func_all_isomorphic

# If this callable is *NOT* pure-Python, raise an exception. C-based
# callables do *NOT* define the "__globals__" dunder attribute.
die_unless_func_python(func=func, exception_cls=exception_cls)
# Else, this callable is pure-Python.

# Lowest-level wrappee callable wrapped by this wrapper callable.
func_wrappee = unwrap_func_all_closures_isomorphic(func)
func_wrappee = unwrap_func_all_isomorphic(func)

# Dictionary mapping from the name to value of each locally scoped
# attribute accessible to this wrappee callable to be returned.
Expand Down

0 comments on commit 973a00e

Please sign in to comment.