Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way to know if a frame is a decorator call (for example in inspect) #3

Open
smarie opened this issue Nov 20, 2019 · 3 comments
Open

Comments

@smarie
Copy link
Owner

smarie commented Nov 20, 2019

In python there is absolutely no way (no way!) to disambiguate between

  • a call to a decorator factory with a function argument:
@my_great_decorator(f)
def g():
    pass
  • and the call to a decorator without argument:
@my_great_decorator
def g():
    pass

This is extremely annoying and many popular libraries implement ugly tricks to work around the problem.

Here is the text that I wrote in this pending issue:


Python decorators are frequently proposed by libraries as an easy way to add functionality to user-written functions: see attrs, pytest, click, marshmallow, etc.

A common pattern in most such libraries, is that they do not want to provide users with two different symbols for the same function. So they end up implementing decorators that can be used both as decorators (no arguments no parenthesis) AND decorator factories (arguments in parenthesis). This is convenient and intuitive for users. Unfortunately this is not something trivial to implement because the python language does not make any difference between a no-parenthesis decorator call and a with-parenthesis decorator factory call.

So these libraries have to rely on "tricks", the most common one being to check existence of a non-default first parameter that is a callable.

Examples: attrs, pytest, marshmallow

Implementing these tricks is a bit ugly, but more importantly it is a waste of development time because when one changes his decorators signatures, the trick has to possibly be changed (order of arguments, default values, etc). Therefore it is quite a brake to agile development in the first phase of a project, where the api is not very stable.

I regrouped all known and possible tricks in a library decopatch to provide a handy way to solve this problem. But it is still "a bunch of tricks". This library, or the manual implementations such as the examples above, could be much faster/efficient if there were at least, a way to determine if a frame is a call to @.

PEP Proposal

At least let's have a inspect.is_decorator_call(frame) feature in the stdlib. That function would return True if the frame is a decorator call using @. For the following example to work:

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

Then is_decorator_call should

  • be callable without arguments (default to current frame)
  • return True only if the current frame is the one directly following the decorator application. In nested frames (such as the one obtained after first recursive call to set_hello_tag above, is_decorator_call should return False.

Note that a more convenient way to solve this problem is also proposed in here : it would be to offer a @decorator_factory helper in the stdlib. But first feedback from python-ideas mailing list showed that this was maybe too disruptive.

@smarie smarie changed the title [decopatch] a way to know if a frame is a decorator call [decopatch] a way to know if a frame is a decorator call (for example in inspect) Nov 20, 2019
@smarie smarie changed the title [decopatch] a way to know if a frame is a decorator call (for example in inspect) A way to know if a frame is a decorator call (for example in inspect) Sep 7, 2020
@plammens
Copy link

plammens commented Sep 18, 2020

Let me play devil's advocate:

There is no fundamental reason to force decorator factories to work directly "without parentheses". If you have a decorator factory my_decorator_factory and you want to apply its default decorator to a function, you should just do:

@my_decorator_factory()
def func():
   ...

The "no-parentheses" pattern is a convention (that, if I had to guess, gained popularity simply because the extra () looks ugly) that violates the conceptual separation of "decorator" and "decorator factory". And the result is: "you need an ugly trick to implement an ugly trick". I see no problem there 😉

@smarie
Copy link
Owner Author

smarie commented Sep 20, 2020

Thanks for the feedback @plammens ! Well the important part is "you need to do something", while most users are so accustomed with this being the reference way to work with decorators, that they expect it to be easy to develop. In other words, this makes it very hard to go from user to developer. It is after all just a design choice: theoretical beauty vs. practical usability. No good answer... except that many theoretically beautiful languages are now dead ;)

@plammens
Copy link

plammens commented Sep 20, 2020

No good answer... except that many theoretically beautiful languages are now dead ;)

Yes, after all, practicality beats purity...

I agree that now it's to late to just "forget about the current convention", and that users just expect this to work.

<rant>

Although it is regrettable that this convention was born at all, because I see absolutely no difference in practical usability between @my_decorator_factory and @my_decorator_factory(). (If two extra parenthesis characters make all the difference in practical usability, then all of Java would be... practically unusable! 😉) If anything, I only see a difference in visual aesthetics.

</rant>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants