As stated in the introduction, without language modification (see proposal) or additional knowledge or source code introspection, python is just not capable of detecting that
@say_hello(foo) is different from
@say_hello applied to function
foo without parenthesis.
However there are well-known "tricks" to perform a disambiguation that covers most standard cases.
decopatch is basically a collection of such tricks. Some of them are static, based on your decorator's signature, and some others are dynamic, based on the received arguments. This section explains what happens behind the scenes.
The reader is encouraged to get familiar with the basic
decopatch principles first by reading the introduction. In particular the two main development styles (nested and flat) are assumed to be understood. All examples below use the "flat" style, but the equivalent "nested" style would lead to the exact same result.
1- no-args decorators¶
It is probably quite useless to use
decopatch if your decorator has no arguments, except if you wish to easily support with- and without- parenthesis usage. This is how you can do it
from decopatch import decorator, DECORATED @decorator def replace_with_hello(f=DECORATED): """ Decorator to replace anything with the 'hello' string """ return 'hello'
You can of course test that it works fine:
@replace_with_hello # no parenthesis def foo(): pass assert foo == 'hello' @replace_with_hello() # with parenthesis def foo(): pass assert foo == 'hello'
In this particular case,
decopatch does not expose a decorator with no arguments as you would expect, but it instead adds a "dummy" var-positional argument named
_, so that both with- and without- parenthesis usages are supported:
>>> help(replace_with_hello) Help on function replace_with_hello in module ...: replace_with_hello(*_) Decorator to replace anything by the 'hello' string.
If your users try to input arguments they will get a
TypeError, except if they provide a single argument that is a callable or a class. In that case we have no means to disambiguate so we prefer to consider that this is a no-parenthesis usage, rather than trying complex disambiguation tricks. After all, your decorator is supposed to have no arguments :)
replace_with_hello(1) # TypeError: function 'replace_with_hello' does not accept any argument. replace_with_hello(print) # no error !
create_no_args_decorator in the source code for details.
2- keyword-only decorators¶
1+ mandatory argument(s)¶
If your decorator is keyword-only and has at least one mandatory argument:
@decorator def replace_with(*, replacement, f=DECORATED): """ Decorator to replace anything with the <replacement> object. """ return replacement
then everything is very easy for
decopatch: it can expose your desired signature directly, users have no way to use it in an ambiguous manner, and a successful call will always be a with-parenthesis call.
@replace_with(replacement='hello') def foo(): pass assert foo == 'hello' replace_with(1) # TypeError: replace_with() takes 0 positional arguments replace_with(str) # TypeError: replace_with() takes 0 positional arguments
create_kwonly_decorator in the source code for details.
0 mandatory arguments¶
If your decorator is keyword-only but has no mandatory argument:
@decorator def replace_with(*, replacement='hello', f=DECORATED): """ Decorator to replace anything with the <replacement> object. Default value is 'hello'. """ return replacement
decopatch automatically adds a dummy var-positional argument named
_ to the decorator signature so that your users can use it without arguments nor parenthesis:
>>>help(replace_with) Help on function replace_with in module ...: replace_with(*_, replacement='hello') Decorator to replace anything with the <replacement> object.
Then disambiguations are handled similarly to all other decorators (see below).
create_kwonly_decorator in the source code for details.
3- General case¶
In the general case, disambiguation is made of two phases: first eliminating cases for which we have no doubts, then handling the remaining ambiguous cases.
disambiguate_call in the source code for details.
a- Eliminating easy cases:
- we look at the number of positional and keyword arguments to eliminate cases that can not be a "no-parenthesis" usage
- we look at the value of the argument received to eliminate further: for example, if it is not a callable nor a class it is has to be provided with parenthesis.
b- Handling still-ambiguous cases
If we reach this part, that's because the first argument in the signature is
- a callable or a class,
- positional (or in python 2, is different from its default value),
- and it is the only one (or in python 2, the only one different from its default value)
Note: python 2 behaviour should align when this
funcsigs issue is fixed.
In that scenario,
decopatch makes the least worst guess: this is probably a no-parenthesis call. Because it is not very probable that your decorator is made to receive a function or a class as first argument.
However you can change this default behaviour, by providing additional knowledge.
First, you can explicitly declare that your decorator is a function-only or class-only decorator, by using
(is_class_decorator=False), or by using the shortcut aliases
@function_decorator respectively. In that case,
decopatch will know that for example a class received by a function decorator is probably a with-parenthesis first argument.
You can also provide an explicit disambiguation function (
custom_disambiguator=...). This function will only be called for ambiguous cases. It should accept a single argument (the first argument's value), and should return either
FirstArgDisambiguation.is_decorated_target. It can also return
FirstArgDisambiguation.is_ambiguous if it can not decide ; in which case an exception will be raised. For your convenience, you can use the predefined disambiguators
custom_disambiguator=no_parenthesis if an ambiguous first argument should always be handled as an arg (with parenthesis) or as the decorated target (no parenthesis) respectively.
Finally as a last resort scenario you can enable introspection (
enable_stack_introspection=True). This beta feature seems to only work reliably with function decorators, because
inspect.stack does not provide a reliable way to access the decorator usage source code line when used on a class.