How can I add keyword arguments to a wrapped function in Python 2.7?
If you only ever specify the additional arguments as keywords, you can get them out of the kw dictionary (see below). If you need them as positional AND keyword arguments, then I think you should be able to use inspect.getargspec on the original function, and then process args and kw in func_wrapper.
Code below tested on Ubuntu 14.04 with Python 2.7, 3.4 (both Ubuntu-provided) and 3.5 (from Continuum).
from functools import wraps
def my_decorator(decorator_arg1=None, decorator_arg2=False):
# Inside the wrapper maker
def _decorator(func):
# Do Something 1
@wraps(func)
def func_wrapper(
*args,
**kwds):
# new_arg1, new_arg2 *CANNOT* be positional args with this technique
new_arg1 = kwds.pop('new_arg1',False)
new_arg2 = kwds.pop('new_arg2',None)
# Inside the wrapping function
# Calling the wrapped function
if new_arg1:
print("new_arg1 True branch; new_arg2 is {}".format(new_arg2))
return func(*args, **kwds)
else:
print("new_arg1 False branch; new_arg2 is {}".format(new_arg2))
# do something with new_arg2
return func(*args, **kwds)
def added_function():
# Do Something 2
print('added_function')
func_wrapper.added_function = added_function
return func_wrapper
return _decorator
@my_decorator(decorator_arg1=4, decorator_arg2=True)
def foo(a, b):
print("a={}, b={}".format(a,b))
def bar():
pass
#foo(1,2,True,7) # won't work
foo(1, 2, new_arg1=True, new_arg2=7)
foo(a=3, b=4, new_arg1=False, new_arg2=42)
foo(new_arg2=-1,b=100,a='AAA')
foo(b=100,new_arg1=True,a='AAA')
foo.added_function()
if __name__=='__main__':
import sys
sys.stdout.flush()
bar()
Output is
new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 False branch; new_arg2 is 42
a=3, b=4
new_arg1 False branch; new_arg2 is -1
a=AAA, b=100
new_arg1 True branch; new_arg2 is None
a=AAA, b=100
added_function
To add arguments to an existing function's signature, while making that function behave like a normal python function (correct help, signature and TypeError
raising in case of wrong arguments provided) you can use makefun
, I developped it specifically to solve this use case.
In particular makefun
provides a replacement for @wraps
that has a new_sig
argument where you specify the new signature. Here is how your example would write:
try: # python 3.3+
from inspect import signature, Parameter
except ImportError:
from funcsigs import signature, Parameter
from makefun import wraps, add_signature_parameters
def my_decorator(decorator_arg1=None, decorator_arg2=False):
# Inside the wrapper maker
def _decorator(func):
# (1) capture the signature of the function to wrap ...
func_sig = signature(func)
# ... and modify it to add new optional parameters 'new_arg1' and 'new_arg2'.
# (if they are optional that's where you provide their defaults)
new_arg1 = Parameter('new_arg1', kind=Parameter.POSITIONAL_OR_KEYWORD, default=False)
new_arg2 = Parameter('new_arg2', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
new_sig = add_signature_parameters(func_sig, last=[new_arg1, new_arg2])
# (2) create a wrapper with the new signature
@wraps(func, new_sig=new_sig)
def func_wrapper(*args, **kwds):
# Inside the wrapping function
# Pop the extra args (they will always be there, no need to provide default)
new_arg1 = kwds.pop('new_arg1')
new_arg2 = kwds.pop('new_arg2')
# Calling the wrapped function
if new_arg1:
print("new_arg1 True branch; new_arg2 is {}".format(new_arg2))
return func(*args, **kwds)
else:
print("new_arg1 False branch; new_arg2 is {}".format(new_arg2))
# do something with new_arg2
return func(*args, **kwds)
# (3) add an attribute to the wrapper
def added_function():
# Do Something 2
print('added_function')
func_wrapper.added_function = added_function
return func_wrapper
return _decorator
@my_decorator(decorator_arg1=4, decorator_arg2=True)
def foo(a, b):
"""This is my foo function"""
print("a={}, b={}".format(a,b))
foo(1, 2, True, 7) # works, except if you use kind=Parameter.KEYWORD_ONLY above (py3 only)
foo(1, 2, new_arg1=True, new_arg2=7)
foo(a=3, b=4, new_arg1=False, new_arg2=42)
foo(new_arg2=-1,b=100,a='AAA')
foo(b=100,new_arg1=True,a='AAA')
foo.added_function()
help(foo)
It works as you would expect:
new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 True branch; new_arg2 is 7
a=1, b=2
new_arg1 False branch; new_arg2 is 42
a=3, b=4
new_arg1 False branch; new_arg2 is -1
a=AAA, b=100
new_arg1 True branch; new_arg2 is None
a=AAA, b=100
added_function
Help on function foo in module <...>:
foo(a, b, new_arg1=False, new_arg2=None)
This is my foo function
So you can see that the exposed signature is as expected, and your users do not see the internals. Note that you can make the two new arguments "keyword-only" by setting kind=Parameter.KEYWORD_ONLY
in the new signature, but as you already know this does not work in python 2.
Finally, you might be interested in making your decorator code more readable and robust to no-parenthesis usages, using decopatch
. Among other things it supports a "flat" style that is well suited in your case because it removes one level of nesting:
from decopatch import function_decorator, DECORATED
@function_decorator
def my_decorator(decorator_arg1=None, decorator_arg2=False, func=DECORATED):
# (1) capture the signature of the function to wrap ...
func_sig = signature(func)
# ...
# (2) create a wrapper with the new signature
@wraps(func, new_sig=new_sig)
def func_wrapper(*args, **kwds):
# Inside the wrapping function
...
# (3) add an attribute to the wrapper
def added_function():
# Do Something 2
print('added_function')
func_wrapper.added_function = added_function
return func_wrapper
(I'm also the author of this one, and created it because I was tired of the nesting and the no-parenthesis handling)