Skip to content

Commit

Permalink
@functool.wraps() classes.
Browse files Browse the repository at this point in the history
This commit generalizes the `@beartype` decorator to reluctantly
acknowledge the existence of horrifying wrapper expressions that pass
types rather than callables to the standard `@functool.wraps` decorator
(e.g., `@functool.wraps(list)`), resolving issue #339 kindly submitted
by the indomitable and fierce of heart @sylvorg. Previously, `@beartype`
raised unreadable exceptions when confronted with this profane edge
case. Now, `@beartype` turns a blind eye to the special darkness in your
codebase by silently ignoring *all* wrapped objects that are not
pure-Python callables. **K-k-k-killer Combo!!** (*Insane inanities!*)
  • Loading branch information
leycec committed Mar 20, 2024
1 parent 8e3cef6 commit fb82b1b
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 8 deletions.
55 changes: 47 additions & 8 deletions beartype/_util/func/utilfuncwrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ def unwrap_func_once(func: Any) -> Callable:
Wrapper callable to be unwrapped.
Returns
----------
-------
Callable
The immediate wrappee callable wrapped by the passed wrapper callable.
Raises
----------
------
_BeartypeUtilCallableWrapperException
If the passed callable is *not* a wrapper.
'''
Expand Down Expand Up @@ -86,7 +86,7 @@ def unwrap_func_all(func: Any) -> Callable:
Wrapper callable to be unwrapped.
Returns
----------
-------
Callable
Either:
Expand Down Expand Up @@ -133,7 +133,7 @@ def unwrap_func_all_isomorphic(func: Any) -> Callable:
Wrapper callable to be unwrapped.
Returns
----------
-------
Callable
Either:
Expand All @@ -143,14 +143,53 @@ def unwrap_func_all_isomorphic(func: Any) -> Callable:
'''

# Avoid circular import dependencies.
from beartype._util.func.utilfunctest import is_func_wrapper_isomorphic
from beartype._util.func.utilfunctest import (
is_func_python,
is_func_wrapper_isomorphic,
)

# While that callable wraps a lower-level callable with a higher-level
# isomorphic wrapper...
# While that callable is a higher-level isomorphic wrapper wrapping a
# lower-level callable...
while is_func_wrapper_isomorphic(func):
# Undo one layer of wrapping by reducing the former to the latter.
# print(f'Unwrapping isomorphic closure wrapper {func} to wrappee {func.__wrapped__}...')
func = func.__wrapped__ # type: ignore[attr-defined]
func_wrapped = func.__wrapped__ # type: ignore[attr-defined]

# If the lower-level object wrapped by this higher-level isomorphic
# wrapper is *NOT* a pure-Python callable, this object is something
# uselessly pathological like a class or C-based callable. Silently
# ignore this useless object by halting iteration. Doing so preserves
# this useful higher-level isomorphic wrapper as is.
#
# Note that this insane edge case arises due to the standard
# @functools.wraps() decorator, which passively accepts possibly C-based
# classes by wrapping those classes with pure-Python functions: e.g.,
# from beartype import beartype
# from functools import wraps
# from typing import Any
#
# @beartype
# @wraps(list)
# def wrapper(*args: Any, **kwargs: Any):
# return list(*args, **kwargs)
#
# In the above example, the higher-level isomorphic wrapper wrapper()
# wraps the lower-level C-based class "list".
#
# Unwrapping this wrapper to this class would induce insanity throughout
# the codebase, which sanely expects wrappers to be callables rather
# than classes. Clearly, classes have *NO* signatures. Technically, a
# pure-Python class may define __new__() and/or __init__() dunder
# methods that could be considered to be the signatures of those
# classes. Nonetheless, C-based classes like "list" have *NO* such
# analogues. The *ONLY* sane approach here is to pretend that we never
# saw this pathological edge case.
if not is_func_python(func_wrapped):
break
# Else, this lower-level callable is pure-Python.

# Reduce this higher-level wrapper to this lower-level wrappee.
func = func_wrapped

# Return this wrappee, which is now guaranteed to *NOT* be an isomorphic
# wrapper but might very well still be a wrapper, which is fine.
Expand Down
34 changes: 34 additions & 0 deletions beartype_test/a00_unit/a70_decor/test_decorwrappee.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,40 @@ def through_the_long_burning_day(*args, **kwargs):
with raises(BeartypeCallHintParamViolation):
when_the_moon(b"Filled the mysterious halls with floating shades")


def test_wrappee_wrapper_type() -> None:
'''
Test the :func:`beartype.beartype` decorator on **type wrappers**
(i.e., types decorated by the standard :func:`functools.wraps` decorator
for wrapping arbitrary types with additional functionality defined by
higher-level decorators, despite the fact that wrapping types does *not*
necessarily make as much coherent sense as one would think it does).
'''

# ....................{ IMPORTS }....................
# Defer test-specific imports.
from beartype import beartype
from beartype.typing import Any
from functools import wraps

# ....................{ WRAPPERS }....................
@beartype
@wraps(list)
def that_echoes_not_my_thoughts(*args: Any, **kwargs: Any):
'''
Arbitrary **decorated type non-closure wrapper** (i.e., wrapper defined
as a function wrapped by an arbitrary type decorated by the
:func:`.beartype` decorator).
'''

return list(*args, **kwargs)

# ....................{ ASSERTS }....................
# Assert that this wrapper passed valid parameters returns the expected
# value.
assert that_echoes_not_my_thoughts(('A', 'gloomy', 'smile',)) == [
'A', 'gloomy', 'smile']

# ....................{ TESTS ~ fail : wrappee }....................
def test_wrappee_type_fail() -> None:
'''
Expand Down

0 comments on commit fb82b1b

Please sign in to comment.