How to Make a Good Decorator¶
[1]:
"""
TITLE : Decorator Reference
AUTHOR : Nathaniel Starkman
PROJECT : utilipy
""";
__author__ = 'Nathaniel Starkman'
__version__ = "Oct 22, 2019"
This is a walkthrough towards building a good decorator. We will do a lot of things wrong before getting them right.
Decorators are a powerful way to augment functions, and even classes. With a decorator we can alter the input or output of a function, and even edit the properties of a function. This discussion presupposes some familiarity with the use and construction of decorators. If you want a primer on decorators, this excellent article will help and will be referenced repeatedly in this discussion.
The ultimate goal is a decorator that can edit the input and output of a function, will inherit the signature, annotations, and docstring of the decorated function, and can modify said docstring and signature.
Note¶
Anything that is in a section called Template is good enough to be copied and used.
External Packages¶
pretty much everything base Python. There are (at least) two mature packages for making decorators: decorator
and wrapt
. I don’t detail how to use them here, thogh this could change if I discover features pertinent to my work that cannot be easily accomplished with base Python.
If you are looking to make unified classmethod and staticmethod and general function decorators, check out wrapt
. For generic function creation, the decorator
package offers a powerful feature called FunctionMaker
(note this function is not compatible with Signature objects, otherwise I would use it here). Astropy offers a make_function_with_signature
that is somewhat similar to FunctionMaker
.
If you are reading this here, then you are probably familiar with another entrant to the decorator field – utilipy
. utilipy
tries to use base-python as much as possible, but has needed to augment the behavior of the functools
and inspect
packages to achieve the more advanced sigantore-modifying features. Some of the advanced decorators here will be built with tools in utilipy
.
In this section we do the necessary setup: imports, function definitions, etc.
Imports¶
If you do not have utilipy
downloaded, you will need to do so now. This is both a shameless plug for my package, but also a necessary check since some of the later decorators will rely on the inspect
and functools
packages contained in utilipy
.
utilipy is a centralized repository for the codes I have found both useful in my work. There are many features, but the only ones we will use today are the inspect
, functools
, and ipython
modules.
[2]:
import utilipy
import inspect, functools
# from utilipy.util import inspect, functools
from utilipy import ipython # module for interacting with an IPython environment
ipython.run_imports(base=True) # rapidly import standard python scripts
ERROR:root:File `'(..,.py'` not found.
set autoreload to 1
For more details, see the documentation or the help functions, like
utilipy.help()
Test Function¶
Decorators need a function to decorate. Here we define a very generic function with all 5 different argument types:
arguments
defaulted arguments
variable arguments,
keyword-only arguments
variable keyword arguments
[3]:
def function(x: '1st arg', y: '2nd arg', # arguments
a: '1st defarg'=10, b=11, # defaulted arguments
*args: 'args', # variable arguments
k: '1st kwonly'='one', l: '2nd kwonly'='two', # keyword-only arguments
**kw: 'kwargs' # variable keyword arguments
) -> tuple:
'''function for testing decoration
This function has all 5 different types of arguments:
1) arguments, 2) defaulted arguments, 3) variable arguments,
4) keyword-only arguments, 5) variable keyword arguments
'''
return x, y, a, b, args, k, l, kw
# /def
Checking the properties of the function,
[4]:
help(function)
Help on function function in module __main__:
function(x: '1st arg', y: '2nd arg', a: '1st defarg' = 10, b=11, *args: 'args', k: '1st kwonly' = 'one', l: '2nd kwonly' = 'two', **kw: 'kwargs') -> tuple
function for testing decoration
This function has all 5 different types of arguments:
1) arguments, 2) defaulted arguments, 3) variable arguments,
4) keyword-only arguments, 5) variable keyword arguments
This function, since it is the base definition of the function, has the correct location, name, signature, and docstring.
Simple Decorators¶
Decorators can be constructed in a few different ways. We will start with the simplest method – nested functions. The outer function – the decorator – constructs an inner function wrapper
– that controls how the wrappedfunction
is implemented.
The general construction is,
def decorator(function):
def wrapper():
# do stuff here
function() # call the function
# and here
return
return wrapper
No-Imports Python¶
These are the simplest decorators, using no imported packages. The results will NOT be fantastic and should not be used.
There are two ways to apply a decorator. The first, by calling the function, is the most obvious and is very useful for modifying existing functions. The second, using the ‘pie’ syntax, is great when defining new functions.
Using the above method, let’s construct a simple decorator:
[5]:
def simple_decorator(function):
def wrapper(*args, **kw):
print("Pre-function")
return_ = function(*args, **kw) # calling the function
print(return_)
print("Post-function")
return return_
return wrapper
# /def
This decorator will print ‘Pre-function’, print the output of the function, then print ‘Post-function’.
Now this decorator needs to be applied to a function. We will start with the Nested Function Method
Nested Function Method
This is the first of the two decorator methods – through function calling. We will make a new function, decorated by simple_decorator
, by calling simple_decorator(function)
.
[6]:
simple_function = simple_decorator(function)
Now calling the function,
[7]:
simple_function(1, 2, k='1st keyword')
Pre-function
(1, 2, 10, 11, (), '1st keyword', 'two', {})
Post-function
[7]:
(1, 2, 10, 11, (), '1st keyword', 'two', {})
Succcess! The print statements were effective and the function was called with the modified inputs and used all the expected default values.
Introspection
Let’s take a closer look at this function.
[8]:
help(simple_function)
Help on function wrapper in module __main__:
wrapper(*args, **kw)
Now for the bad news.
When we compare this to function
(above), this decorator has lost most of the information. The function now has a different signature and the docstring from the wrapper. This is not great.
Decorator Syntax Method
Before we go and make a better decorator, this is how to use the ‘pie’ decorator syntax.
[9]:
@simple_decorator
def simple_dec_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
Now calling the function
[10]:
simple_dec_function(1, 2)
Pre-function
(1, 2, 10, 11, (), 'one', 'two', {})
Post-function
[10]:
(1, 2, 10, 11, (), 'one', 'two', {})
As expected, the output is exactly the same as in the previous example.
Introspection
[11]:
help(simple_dec_function)
Help on function wrapper in module __main__:
wrapper(*args, **kw)
Again, the introspection fails quite badly.
We can do better.
Functools Decorators¶
The easiest way to make a better decorator is with the built-in package functools
. Using the function wraps
from the functools
package, our simple decorators can now preserve function signatures and docstrings.
Now, the decorator construction syntax is
def decorator(function):
@functools.wraps(function)
def wrapper():
# do stuff here
function()
# and here
return
return wrapper
Yes, we now have a decorator inside our decorator-making function. If this were a tutorial on decorators and decorator factories, the decorator nesting would have only just begun.
Let’s see this decorator in action.
Nested Function Method
Again, starting with the function calling method,
[12]:
def ftw_decorator(function):
'''FuncTools.Wraps (ftw) decorator'''
@functools.wraps(function)
def wrapper(*args, **kw):
print("Pre function call")
print(function(*args, **kw))
print("Post function call")
return
return wrapper
Now applying the decorator to function
.
[13]:
ftw_function = ftw_decorator(function)
Does it work?
[14]:
ftw_function(1, 2)
Pre function call
(1, 2, 10, 11, (), 'one', 'two', {})
Post function call
Unsurprisingly, it does.
Now we can see whether it preserves the signature and docstring.
[15]:
help(ftw_function)
Help on function function in module __main__:
function(x: '1st arg', y: '2nd arg', a: '1st defarg' = 10, b=11, *args: 'args', k: '1st kwonly' = 'one', l: '2nd kwonly' = 'two', **kw: 'kwargs') -> tuple
function for testing decoration
This function has all 5 different types of arguments:
1) arguments, 2) defaulted arguments, 3) variable arguments,
4) keyword-only arguments, 5) variable keyword arguments
The new function matches the original function!
Decorator Syntax Method
Checking how the ‘pie’-syntax works when using a functools decorator.
[16]:
@ftw_decorator
def ftw_dec_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
[17]:
ftw_dec_function(1, 2)
Pre function call
(1, 2, 10, 11, (), 'one', 'two', {})
Post function call
The decorated function works as expected.
Introspection
[18]:
help(ftw_dec_function)
Help on function ftw_dec_function in module __main__:
ftw_dec_function(x, y, a=10, b=11, *args, k='one', l='two', **kw)
function for testing decoration
This function has all 5 different types of arguments
Again, the decorator preserves the signature and docstring. Great!
Template¶
Let’s apply what we’ve learned in the previous section. Here is a template for constructing a basic decorator.
[19]:
def template_decorator(function):
'''Docstring for decorator.
Description of this decorator
'''
@functools.wraps(function)
def wrapper(*args, **kw):
"""Wrapper docstring.
This docstring is not visible.
But is an excellent place to add some internal documentation.
"""
# do something here
return_ = function(*args, **kw)
# do something and here
return return_
return wrapper
This decorator does many things. In particular,
This decorator does:
anything to the function input and output
make a function that looks exactly like the input function
This is integral for quality introspection.
This decorator does not:
- work when created with parenthesisexample:
@example_decorator() def example_function(): pass
- accept (kw)arguments on applicationexample:
@example_decorator(arg='arg') def example_function(): pass
add any extra (kw)arguments to control the
wrapper
wrapper
cannot accept any arguments other than those intofunction
.document what the wrapper is doing.
In a way that is introspectable, such as modifying the docstring or signature.
Decorators with Optional Arguments¶
The decorators constructed thus far accept no arguments except the function to be decorated. For basic problems this is sufficient, but for more complex use cases we want decorators that accept arguments.
As as an example, this decorator adds a keyword argument added_kw
that modifies the behavior of wrapper.
def decorator(function, added_kw='kwarg'):
@functools.wraps(function)
def wrapper(*args, **kw):
print(added_kw)
return_ = function(*args, **kw)
return return_
return wrapper
This allows for modification of a decorator on a function-by-function basis.
[20]:
# define decorator
def opt_decorator(function, added_kw='kwarg'):
@functools.wraps(function)
def wrapper(*args, **kw):
print(added_kw)
return function(*args, **kw)
return wrapper
# try to apply decorator to function
try:
@opt_decorator(added_kw='KWARG')
def opt_dec_function(*args, **kw):
print(args, kw)
except TypeError as e: # intercept error for `restart, run all` in notebook
ipython.printMD(e, color='red', size=18, highlight='yellow')
opt_decorator() missing 1 required positional argument: ‘function’
This problem needs to be addressed. As it happens, there is a simple solution that both fixes the problem of applying the decorator, but also make the decorator usable both with and without arguments. Now these are flexible!
For a decorator (cleverly called decorator
), our decorators will be able to be applied in one of two ways:
Basic decorator calling
@decorator def example_function(): pass
Where the decorator is a function
the arguments are optional and do not need to be passed
@decorator() def example_function(): pass
but we have that option
@decorator(arg='arg') def example_function(): pass
That’s a lot of talk. Time to deliver.
The simplest way is to construct the decorator inside a function. That’s another layer to the nested function party. This method is called decorator factories.
[21]:
# making the decorator
def optional_argument_decorator(func=None, *, kw1=None, kw2=None): # decorator factory
def decorator(function): # decorator function
@functools.wraps(function) # match function properties
def wrapper(*args, **kw): # wrapper
print(f'added kwargs: kw1={kw1}, kw2={kw2}')
return_ = function(*args, **kw)
return return_
return wrapper
# allowing for optional arguments
if func is None:
return decorator # if calling decorator factory (paranthesis)
else:
return decorator(func) # if not calling decorator factory (no paranthesis)
# /def
# applying to a function
@optional_argument_decorator(kw1='changed')
def opt_arg_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# calling the function
opt_arg_function(1, 2)
added kwargs: kw1=changed, kw2=None
[21]:
(1, 2, 10, 11, (), 'one', 'two', {})
The decorated function works as expected.
Introspection
[22]:
help(opt_arg_function)
Help on function opt_arg_function in module __main__:
opt_arg_function(x, y, a=10, b=11, *args, k='one', l='two', **kw)
function for testing decoration
This function has all 5 different types of arguments
While this function works as advertised, there is no way to tell that the keyword arguments kw1
and kw2
have been added.
Template¶
The previous method is almost good enough for a decorator template – it would be if only there weren’t a cleaner method that didn’t involve so many nested functions.
Alternatively, as mentioned in the good article I have referenced a few times, there is a better way to make these argument-optional decorators. This method is included in Recipe 9.6 of the Python Cookbook.
This is a decorator template implementing this method.
[23]:
def template_decorator(function=None, *, added_kw=None):
''''Docstring for decorator.
Description of this decorator
Parameters
----------
function : types.FunctionType or None, optional
the function to be decoratored
if None, then returns decorator to apply.
kw1, kw2 : any, optional
key-word only arguments
Returns
-------
wrapper : types.Function
wrapper for function
does a few things
'''
if function is None: # allowing for optional arguments
return functools.partial(template_decorator, added_kw=added_kw)
@functools.wraps(function)
def wrapper(*args, **kw):
"""Wrapper docstring.
This docstring is not visible.
But is an excellent place to add some internal documentation.
"""
# do stuff here
return_ = function(*args, **kw)
# and here
return return_
# /def
return wrapper
# /def
Now we have decorators that can optionally accept arguments.
This decorator does:
anything to the function input and output
make a function that looks exactly like the input function
for quality introspection.
work when created with parenthesis
accept (kw)arguments on application
This decorator does not:
add any extra (kw)arguments to control the
wrapper
wrapper
cannot accept any arguments other than those intofunction
.document what the wrapper is doing.
In a way that is introspectable, such as modifying the docstring or signature.
Optional Arguments into Wrapper¶
Rather than a decorator that accepts arguments, let us now consider decorators which add arguments to the function wrapper.
For instance, we start with a function
def function(x, y):
return x+y
but wish to change the function to accept an optional third argument and add this to the sum x+y
.
That is, using a decorator, make a function
def function(x, y, z=0):
return x+y+z
Implementing this,
[24]:
# make a decorator
def wrapper_arg_decorator(function):
@functools.wraps(function)
def wrapper(*args, z=0, **kw):
return_ = function(*args, **kw)
return return_ + z
return wrapper
# make function
@wrapper_arg_decorator
def wrapper_arg_function(x, y):
"""wrapper_arg_function
adds x & y
"""
return x+y
# calling
print('Calling function: ', wrapper_arg_function(1, 2))
print('Calling function with z: ', wrapper_arg_function(1, 2, z=3))
Calling function: 3
Calling function with z: 6
The addition of z
works!
[25]:
help(wrapper_arg_function)
Help on function wrapper_arg_function in module __main__:
wrapper_arg_function(x, y)
wrapper_arg_function
adds x & y
Unfortunately, like in the previous section, there is no way to tell that z
has been added to the funciton.
Template¶
This decorator-template adds keyword arguments into the wrapper. As a nice touch, this also demonstrates how default values for the wrapper can be set upon function creation.
[26]:
def template_decorator(function=None, *, kw1=None, kw2=None):
''''Docstring for decorator.
Description of this decorator
Parameters
----------
function : types.FunctionType or None, optional
the function to be decoratored
if None, then returns decorator to apply.
kw1, kw2 : any, optional
key-word only arguments
sets the wrappeer's default values.
Returns
-------
wrapper : types.FunctionType
wrapper for function
does a few things
includes the original function in a method `.__wrapped__`
'''
if function is None: # allowing for optional arguments
return functools.partial(template_decorator, kw1=k1, kw2=kw2)
@functools.wraps(function)
def wrapper(*args, kw1=kw1, kw2=kw2, **kw):
"""Wrapper docstring.
This docstring is not visible.
But is an excellent place to add some internal documentation.
"""
# do stuff here
return_ = function(*args, **kw)
# and here
return return_
# /def
return wrapper
# /def
Now we have decorators that can optionally accept arguments.
This decorator does:
anything to the function input and output
make a function that looks exactly like the input function
for quality introspection.
work when created with parenthesis
accept (kw)arguments on application
add any extra (kw)arguments to control the
wrapper
also make the defaults be dynamically set on function creation.
This decorator does not:
document what the wrapper is doing.
In a way that is introspectable, such as modifying the docstring or signature.
Wrapper-Documenting Decorators¶
Now to tackle the last outstanding point – documenting how the wrapper modifies the function. Recall that the wrapper’s docstring is overwritten by functools.wraps
and replaces it with the original function’s docstring.
Docstring Modifications¶
It is actually quite simple to modify the docstring of a function since the docstring is just stored as the __doc__
attribution of the function.
As always, we are building on the successses of the previous section.
[27]:
def doc_mod_decorator(function=None, *, kw1='default kw1', kw2='default kw2'):
if function is None: # allowing for optional arguments
return functools.partial(example_decorator, kw1=k1, kw2=kw2)
@functools.wraps(function)
def wrapper(*args, kw1=kw1, kw2=kw2, **kw):
return_ = function(*args, **kw)
return return_
# /def
if wrapper.__doc__ is None:
wrapper.__doc__ = ''
wrapper.__doc__ += ("\nDecorator\n---------\n"
"prints information about function\n"
f"kw1, kw2: defaults {kw1, kw2}\n")
return wrapper
While this will work, there’s a cleaner way. We will use the functools.wraps
from utilipy
, which allows the docstring to be set on wrap, not only inherited from the wrapper function.
[28]:
def doc_mod_decorator(function=None, *, kw1='default kw1', kw2='default kw2'):
if function is None: # allowing for optional arguments
return functools.partial(example_decorator, kw1=k1, kw2=kw2)
@utilipy.utils.functools.wraps(function, signature=False) # preventing signature updating. That is showed later.
def wrapper(*args, kw1=kw1, kw2=kw2, **kw):
"""wrapper
Notes
-----
decorator prints information about function
kw1, kw2: str
defaults kw1='{kw1}', kw2='{kw2}'
"""
print(f'added kwargs: kw1={kw1}, kw2={kw2}')
return_ = function(*args, **kw)
return return_
return wrapper
How does this work?
utilipy
has a modified implementation of functools.wraps
that allows some properties of the wrapper to be set manually, and will still default to inheriting from the original function. The utilipy
and standard functools
are otherwise identical and behave identically for any inputs supported by both modules.
For the actual docstring override, this decorator uses a clever trick: __doc__ or ''
that will always select the intended docstring. Docstrings can be either None
or a string; __doc__ or ''
will replace None
with ''
, but keep an actual string over the blank string. This allows for one-line string additions.
Seeing this decorator in action,
[29]:
@doc_mod_decorator
def doc_mod_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
[30]:
doc_mod_function(1, 2, kw1='test')
added kwargs: kw1=test, kw2=default kw2
[30]:
(1, 2, 10, 11, (), 'one', 'two', {})
[31]:
help(doc_mod_function)
Help on function doc_mod_function in module __main__:
doc_mod_function(x, y, a=10, b=11, *args, k='one', l='two', **kw)
function for testing decoration
This function has all 5 different types of arguments
Notes
-----
decorator prints information about function
kw1, kw2: str
defaults kw1='{kw1}', kw2='{kw2}'
As simply as that, we have modified the docstring of the function.
Unfortunately, this is a very brittle system since it must be hardcoded. Good docstrings follow some standardized format – numpy, google, etc. To properly document a function wrapper, we need a lot more than just a simple string append.
I have plans to use https://github.com/numpy/numpydoc/ to build functions that can correctly insert the documentation into a numpy-style docstring. They are not yet implemented.
We can, and will do a fair bit better in a few sections. Stay tuned.
Signature-Modifications¶
Modifying the docstring is all well and good, but we also want to modify the functions’s signature. This is important for autocompletion, strct typing, and just generally good code.
Modifying a function’s signature is non-trivial. With the inspect
module we can expedite the process by assigning to the __signature__
method of the function. For simple applications this is sufficient, but I will show how this can lead to incorrect default values.
But first, a success story,
[32]:
# defining a signature overriding decorator
def sig_override_decorator(function=None):
if function is None: # allowing for optional arguments
return functools.partial(sig_override_decorator)
@functools.wraps(function)
def wrapper(*args, kw1=None, **kw):
print(f'added kwarg: kw1={kw1}')
return_ = function(*args, **kw)
return return_
# getting the signature
sig = utilipy.utils.inspect.fuller_signature(wrapper)
# making new Parameter
param = inspect.Parameter('kw1', inspect._KEYWORD_ONLY, default=None, annotation='added kwarg')
# adding parameter to signature (non-mutable, so returns new signature)
sig = sig.insert_parameter(sig.index_var_keyword, param)
# assigning to wrapper signature
wrapper.__signature__ = sig
return wrapper
# making function
@sig_override_decorator
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# calling function
sig_override_function(1, 2, kw1=2)
added kwarg: kw1=2
[32]:
(1, 2, 10, 11, (), 'one', 'two', {})
Most importantly, how does this introspect?
[33]:
help(sig_override_function)
Help on function sig_override_function in module __main__:
sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', kw1: 'added kwarg' = None, **kw)
function for testing decoration
This function has all 5 different types of arguments
Success! We have added kw1
to the function’s signature.
Time for the bad news.
This method only worked because the parameter we were adding was: 1. Already in the function 2. the default value in the signature matched the default value in the function creation.
Let’s break these assumptions and see how this method fails.
Breaking Assumption 1
Here it is demonstrated how overriding the signature of a function can fail if the function does not already have the argument.
We define a function without kw1
as a keyword-only argument. Overriding the signature will appear to work, but when the function is called, it will throw an exception because the signature does not actually correspond to the function.
[34]:
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two'):
'''function for testing decoration
This function has 4 different types of arguments, excluding variable keyword arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# getting the signature
sig = utilipy.utils.inspect.fuller_signature(sig_override_function)
# making new Parameter
param = inspect.Parameter('kw1', inspect._KEYWORD_ONLY, default=None, annotation='added kwarg')
# adding parameter to signature (non-mutable, so returns new signature)
sig = sig.insert_parameter(sig.index_keyword_only[-1]+1, param)
# assigning to wrapper signature
sig_override_function.__signature__ = sig
help(sig_override_function)
Help on function sig_override_function in module __main__:
sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', kw1: 'added kwarg' = None)
function for testing decoration
This function has 4 different types of arguments, excluding variable keyword arguments
[35]:
try:
sig_override_function(1, 2, kw1=10)
except TypeError:
ipython.printMD("TypeError: sig_override_function() got an unexpected keyword argument 'kw1'", color='red')
TypeError: sig_override_function() got an unexpected keyword argument ‘kw1’
The best we could have done was to include a variable keyword argument and let that absorb kw1
.
[36]:
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has 4 different types of arguments, excluding variable keyword arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# setting __signature__
sig = utilipy.utils.inspect.fuller_signature(sig_override_function)
param = inspect.Parameter('kw1', inspect._KEYWORD_ONLY, default=None, annotation='added kwarg')
sig = sig.insert_parameter(sig.index_end_keyword_only, param)
sig_override_function.__signature__ = sig
# calling
sig_override_function(1, 2, kw1=10)
[36]:
(1, 2, 10, 11, (), 'one', 'two', {'kw1': 10})
While this works, it is clearly not ideal. The signature should match the function
Breaking Assumption 2
Here it is demonstrated how overriding the signature of a function can fail if the overriding signature has mismatching default values.
The overriding signature will try to change the default value of kw1
from None
to 'has a value'
. Again, this will appear to work but actually fail.
[37]:
# defining a signature overriding decorator
def sig_override_decorator(function=None):
if function is None: # allowing for optional arguments
return functools.partial(sig_override_decorator)
@functools.wraps(function)
def wrapper(*args, kw1=None, **kw):
print(f'added kwarg: kw1={kw1}')
return_ = function(*args, **kw)
return return_
sig = utilipy.utils.inspect.fuller_signature(wrapper)
param = inspect.Parameter('kw1', inspect._KEYWORD_ONLY, default='has a value', annotation='added kwarg')
sig = sig.insert_parameter(sig.index_var_keyword, param)
wrapper.__signature__ = sig
return wrapper
# making function
@sig_override_decorator
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# calling function
help(sig_override_function)
Help on function sig_override_function in module __main__:
sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', kw1: 'added kwarg' = 'has a value', **kw)
function for testing decoration
This function has all 5 different types of arguments
See? appears to work
[38]:
sig_override_function(1, 2,)
added kwarg: kw1=None
[38]:
(1, 2, 10, 11, (), 'one', 'two', {})
But actually does not not.
The Fix
How do we fix these problems?
Again we are in luck: utilipy.utils.functools.wraps
has a signature argument for overloading the signature.
[39]:
# make decorator
def sig_override_decorator(function=None):
if function is None: # allowing for optional arguments
return functools.partial(sig_override_decorator)
sig = utilipy.utils.inspect.fuller_signature(function)
param = inspect.Parameter('kw1', inspect._KEYWORD_ONLY, default='has a value', annotation='added kwarg')
sig = sig.insert_parameter(sig.index_var_keyword, param)
@utilipy.utils.functools.wraps(function, signature=sig)
def wrapper(*args, kw1=None, **kw):
print(f'added kwarg: kw1={kw1}')
return_ = function(*args, **kw)
return return_
return wrapper
# making function
@sig_override_decorator
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# calling function
sig_override_function(1, 2)
added kwarg: kw1=has a value
[39]:
(1, 2, 10, 11, (), 'one', 'two', {})
[40]:
help(sig_override_function)
Help on function sig_override_function in module __main__:
sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', kw1: 'added kwarg' = 'has a value', **kw)
function for testing decoration
This function has all 5 different types of arguments
The best thing is, if signature=True
(the default) in utilipy.utils.functools.wraps
, then any keyword-only argument is automagically added to the signature.
[41]:
# make decorator
def sig_override_decorator(function=None):
if function is None: # allowing for optional arguments
return functools.partial(sig_override_decorator)
@utilipy.utils.functools.wraps(function, signature=True) # explicitly setting to True (the default)
def wrapper(*args, kw1='has a default', **kw):
print(f'added kwarg: kw1={kw1}')
return_ = function(*args, **kw)
return return_
return wrapper
# making function
@sig_override_decorator
def sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', **kw):
'''function for testing decoration
This function has all 5 different types of arguments
'''
return x, y, a, b, args, k, l, kw
# /def
# calling function
sig_override_function(1, 2)
help(sig_override_function)
added kwarg: kw1=has a default
[41]:
(1, 2, 10, 11, (), 'one', 'two', {})
Help on function sig_override_function in module __main__:
sig_override_function(x, y, a=10, b=11, *args, k='one', l='two', kw1='has a default', **kw)
function for testing decoration
This function has all 5 different types of arguments
Template¶
This decorator-template adds keyword arguments into the wrapper. As a nice touch, this also demonstrates how default values for the wrapper can be set upon function creation.
[42]:
from utilipy.utils.functools import wraps
def template_decorator(function=None, *, kw1=None, kw2=None):
''''Docstring for decorator.
Description of this decorator
Parameters
----------
function : types.FunctionType or None, optional
the function to be decoratored
if None, then returns decorator to apply.
kw1, kw2 : any, optional
key-word only arguments
sets the wrappeer's default values.
Returns
-------
wrapper : types.FunctionType
wrapper for function
does a few things
includes the original function in a method `.__wrapped__`
'''
if function is None: # allowing for optional arguments
return functools.partial(template_decorator, kw1=k1, kw2=kw2)
@wraps(function)
def wrapper(*args, kw1=kw1, kw2=kw2, kw3='not in decorator factory', **kw):
"""wrapper docstring.
Decorator
---------
prints information about function
kw1, kw2: defaults {kw1}, {kw2}
"""
# do stuff here
return_ = function(*args, **kw)
# and here
return return_
# /def
return wrapper
# /def
Now we have decorators that can optionally accept arguments.
This decorator does:
anything to the function input and output
make a function that looks exactly like the input function
for quality introspection.
work when created with parenthesis
accept (kw)arguments on application
add any extra (kw)arguments to control the
wrapper
also make the defaults be dynamically set on function creation.
document what the wrapper is doing.
In a way that is introspectable, by modifying both the signature and docstring.
This decorator does not:
. . .
[43]:
# Example
@template_decorator
def function(x: '1st arg', y: '2nd arg', # arguments
a: '1st defarg'=10, b=11, # defaulted arguments
*args: 'args', # variable arguments
k: '1st kwonly'='one', l: '2nd kwonly'='two', # keyword-only arguments
**kw: 'kwargs' # variable keyword arguments
) -> tuple:
'''function for testing decoration
This function has all 5 different types of arguments:
1) arguments, 2) defaulted arguments, 3) variable arguments,
4) keyword-only arguments, 5) variable keyword arguments
'''
return x, y, a, b, args, k, l, kw
# /def
help(function)
Help on function function in module __main__:
function(x: '1st arg', y: '2nd arg', a: '1st defarg' = 10, b=11, *args: 'args', k: '1st kwonly' = 'one', l: '2nd kwonly' = 'two', kw1=None, kw2=None, kw3='not in decorator factory', **kw: 'kwargs') -> tuple
function for testing decoration
This function has all 5 different types of arguments:
1) arguments, 2) defaulted arguments, 3) variable arguments,
4) keyword-only arguments, 5) variable keyword arguments
Decorator
---------
prints information about function
kw1, kw2: defaults None, None
Decorator Function Wrap-Up¶
Congrats! You can now build an excellent decorator function that will properly document how it augments the wrapped function.