In Python 3.4+, functools.wraps
preserves the signature of the function it wraps. Unfortunately, if you create decorators that are meant to be stacked on top of each other, the second (or later) decorator in the sequence will be seeing the generic *args
and **kwargs
signature of the wrapper and not preserving the signature of the original function all the way at the bottom of the sequence of decorators. Here's an example.
from functools import wraps
def validate_x(func):
@wraps(func)
def wrapper(*args, **kwargs):
assert kwargs['x'] <= 2
return func(*args, **kwargs)
return wrapper
def validate_y(func):
@wraps(func)
def wrapper(*args, **kwargs):
assert kwargs['y'] >= 2
return func(*args, **kwargs)
return wrapper
@validate_x
@validate_y
def foo(x=1, y=3):
print(x + y)
# call the double wrapped function.
foo()
This gives
-------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-5-69c17467332d> in <module>
22
23
---> 24 foo()
<ipython-input-5-69c17467332d> in wrapper(*args, **kwargs)
4 @wraps(func)
5 def wrapper(*args, **kwargs):
----> 6 assert kwargs['x'] <= 2
7 return func(*args, **kwargs)
8 return wrapper
KeyError: 'x'
and if you switch the order of the decorators, you get the same key error for 'y'
.
I tried replacing wraps(func)
with wraps(func.__wrapped__)
in the second decorator, but this still doesn't work (not to mention it requires the programmer to explicitly know where in the stack of decorators they are working for given wrapper functionality).
I also took a look at inspect.signature(foo)
and this seems to give the right thing, but I found that this is because inspect.signature
has a follow_wrapped
parameter that defaults to True
so it somehow knows to follow the sequence of wrapped functions, but apparently the regular method call framework for invoking foo()
will not follow this same protocol for resolve args and kwargs of the outer decorated wrapper.
How can I just have wraps
faithfully passthrough the signature so that wraps(wraps(wraps(wraps(f))))
(so to speak) always faithfully replicated the signature of f
?