PEP proposal draft¶
As already stated in this documentation, there is as of python 3.7, absolutely no way to detect that the usage of @say_hello
in:
@say_hello(foo)
def bar():
pass
is different from
@say_hello
def foo():
pass
As we saw in the disambiguation principles, a library can disambiguate only if the decorator developer provides additional knowledge.
Below are a few ideas that would fix this problem once and for all (hopefully!) directly in the python language instead. Feedback welcome !
1- breaking backwards compatibility¶
The easiest way to fix the no-parenthesis case directly in the language would be to automatically redirect all decorator usage without parenthesis, to empty-parenthesis. So if you have a set_hello_tag(tag='world')
decorator, if you use it without parenthesis:
@set_hello_tag
def foo():
pass
the python language interpreter would interprete it exactly the same way than
@set_hello_tag()
def foo():
pass
In other words, using a decorator with the @
syntax and without parenthesis would simply be an alias for the same usage with empty parenthesis.
Implementation would therefore be extremely easy: for all decorators, you would always code it in a "nested" way:
def set_hello_tag(tag='world'):
def decorate(f):
setattr(f, 'hello', tag) # set a hello=<tag> tag on the decorated f
return f
return decorate
Drawbacks: this change in the python interpreter behaviour would obviously break compatibility for legacy decorators that did not support arguments at all, such as this one:
def set_helloworld_tag(f):
setattr(f, 'hello', 'world') # set a hello=world tag on the decorated f
return f
2- preserving backwards compatibility¶
If we need to preserve backwards compatibility, then we need to make the new mechanism optional, so that developers explicitly choose to use it.
My suggestion would be to introduce a new @decorator_factory
decorator in the stdlib
, that developers would use to declare that they are ok with redirecting all no-parenthesis usages to with-empty-parenthesis usages. Note that I use the term "factory" because some users use it to distinguish between the no-argument ones (decorators) and the with-argument ones (decorator factories).
This is how you would create a set_hello_tag
decorator:
@decorator_factory
def set_hello_tag(tag='world'):
def decorate(f):
setattr(f, 'hello', tag) # set a hello tag on the decorated f
return f
return decorate
The explicit @decorator_factory
annotation would make the interpreter/stdlib redirect all occurences of @set_hello_tag
(without parenthesis) into explicit @set_hello_tag()
(with empty parenthesis).
Notes:
-
if this is a too low-level feature it might require a dedicated language symbol instead of a "normal" decorator ; but it seems overkill - it would be better if we can avoid creating a new language element.
-
alternatively or in addition, the python
stdlib
could provide a method that would returnTrue
if and only if a given frame is a no-parenthesis decorator call. This method, for example namedinspect.is_decorator_call(frame=None)
, could then be used by the various helper libraries, includingdecopatch
.
Follow-up
After proposing the above to the python-ideas mailing list, it seems that people were not as interested as I thought they would be. I therefore proposed a very minimal feature in the python bug tracker (the inspect.is_decorator_call(frame)
option discussed above). At least with this, everyone would be able to solve the issue easily himself, like this:
from inspect import is_decorator_call
def set_hello_tag(tag='world'):
if is_decorator_call():
# called without parenthesis!
# the decorated object is `tag`
return set_hello_tag()(tag) # note that `is_decorator_call` should not return True for this call
else:
def decorate(f):
setattr(f, 'hello', tag) # set a hello tag on the decorated f
return f
return decorate
Note that is_decorator_call
should not return True
in nested frames, just for the immediate frame in a decorator application.