Motivation¶
This page explains why I felt frustrated by the current tools that we developers have at our disposal to develop decorators in python, and eventually why I ended-up writing decopatch
.
Problem¶
In python, a decorator used without arguments such as
@say_hello # (1) no-parenthesis
def foo(a, b):
pass
requires a completely different implementation code than
@say_hello() # (2) empty-parenthesis
def foo(a, b):
pass
Indeed (1) requires say_hello
to directly return a replacement for the decorated object (in that case function foo
), while (2) requires say_hello
to return a function that returns a replacement for the decorated object ! This is one more level of nesting. If you wish to handle both situations in a robust way, you end-up having to design some ridiculously complex code, relying on some well-known "tricks" based either on checking the type or existence of first argument provided. For example:
def say_hello(f=None):
if f is not None:
# this is (1) @say_hello (without parenthesis)
# we have to directly return a replacement for f
def new_f(...):
...
return new_f
else:
# this is (2) @say_hello() (empty parenthesis)
# we have to return a decorator function
def _decorate(f):
def new_f(...):
...
return new_f
return _decorate
Unfortunately, the 'trick' to use is different for almost every type of decorator signature (var-args, keyword-only, all-optional, ...). So if you change your mind about your API during development time (this often happens, at least to me :)), you end up having to change this useless piece of code several times!
Solution¶
decopatch
provides a simple way to solve this issue. It always uses the best "trick", so that you do not have to care, you just implement one case:
from decopatch import function_decorator
@function_decorator
def say_hello():
def _decorate(f):
def new_f(...):
...
return new_f
return _decorate
To ease things even more, decopatch
also supports a flat mode:
from decopatch import function_decorator, DECORATED
@function_decorator
def say_hello(f=DECORATED):
def new_f(...):
...
return new_f
In both cases, generated decorators have a proper help
and signature
, so users do not see the difference, the choice of mode is a matter of development style.
Why something new ?¶
As opposed to the great decorator
and wrapt
libraries, decopatch
does not try at the same time to help you create decorators and (signature-preserving) function wrappers. In my opinion creating function wrappers is a completely independent topic, you can wish to do it in with a decorator OR without. Nevertheless since it is an important use case, the documentation shows how to do it. If you're interested in this topic, see makefun
, my fork of decorator
's core engine supporting additional use cases such as signature modification.
Also, note that (at the time of writing so with decorator
4.3.2 and wrapt
1.11.1):
- none of
wrapt
anddecorator
preserve the exposed decorator's signature: they both rely on an extra optional argument (func=None
andwrapped=None
respectively). decorator
relies on the__wrapped__
special field to trick thesignature
module about the true signature of the decorator. You can check it by deleting this field on your decorator, and query its signature again: you will see the true signature of your decorator (the one seen by the python interpreter when calling the decorator).- none of
decorator
andwrapt
handle the no-parenthesis case natively.wrapt
explains which lines of codes you have to enter while withdecorator
you have to write the parenthesis.
Could we solve this directly in the python language instead ?¶
That would be great ! See my PEP proposal draft.