Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check if a function was called as a decorator

In the following minimal example decorate is called two times. First using @decorate, second by normal function call decorate(bar).

def decorate(func):
    print(func.__name__)
    return func

@decorate
def bar():
    pass

decorate(bar)

Is it possible to see inside of decorate if the call was invoked by using @decorate or as a normal function call?

like image 202
Tobias Hermann Avatar asked Sep 05 '18 19:09

Tobias Hermann


People also ask

How do you test if a function has been called?

You can log a message when the function is called using: Debug. Log("Function called!"); You can store a bool that starts as false and set it to true when you enter the function. You can then check this bool elsewhere in code to tell whether your function has been called.

Which function is a decorator?

By definition, a decorator is a function that takes another function and extends the behavior of the latter function without explicitly modifying it.

How do you check if a function is being called Python?

has_been_called = True return func(*args) wrapper. has_been_called = False return wrapper @calltracker def doubler(number): return number * 2 if __name__ == '__main__': if not doubler. has_been_called: print "You haven't called this function yet" doubler(2) if doubler. has_been_called: print 'doubler has been called!'

Is decorator a function in Python?

A decorator in Python is a function that takes another function as its argument, and returns yet another function . Decorators can be extremely useful as they allow the extension of an existing function, without any modification to the original function source code.


3 Answers

The @decorator syntax is just syntactic sugar, thus both examples have identical behaviour. This also means whatever distinction you are doing between them might not be as meaningful as you thought.

Although, you can use inspect to read your script and see how the decorator was called in the above frame.

import inspect

def decorate(func):
    # See explanation below
    lines = inspect.stack(context=2)[1].code_context
    decorated = any(line.startswith('@') for line in lines)

    print(func.__name__, 'was decorated with "@decorate":', decorated)
    return func

Note that we had to specify context=2 to the inspect.stack function. The context argument indicates how many lines of code around the current line must be returned. In some specific cases, such as when decorating a subclass, the current line was on the class declaration instead of the decorator. The exact reason for this behaviour has been explored here.

Example

@decorate
def bar():
    pass

def foo():
    pass
foo = decorate(foo)

@decorate
class MyDict(dict):
    pass

Output

bar was decorated with "@decorate": True
foo was decorated with "@decorate": False
MyDict was decorated with "@decorate": True

Caveat

There are still some corner cases that we can hardly overcome such as linebreaks between the decorator and a class declaration.

# This will fail
@decorate

class MyDict(dict):
    pass
like image 120
Olivier Melançon Avatar answered Nov 07 '22 03:11

Olivier Melançon


Olivier's answer took the thoughts right out of my head. However, as inspect.stack() is a particularly expensive call, I would consider opting to use something along the lines of:

frame = inspect.getframeinfo(inspect.currentframe().f_back, context=1)
if frame.code_context[0][0].startswith('@'): 
    print('Used as @decorate: True')
else:
    print("Used as @decorate: False")
like image 22
Philip DiSarro Avatar answered Nov 07 '22 03:11

Philip DiSarro


Contrary to popular believe, @decorator and decorator(…) are not exactly equivalent. The first runs before name binding, the latter after name binding. For the common use-case of top-level functions, this allows to cheaply test which case applies.

import sys

def decoraware(subject):
    """
    Decorator that is aware whether it was applied using `@deco` syntax
    """
    try:
        module_name, qualname = subject.__module__, subject.__qualname__
    except AttributeError:
        raise TypeError(f"subject must define '__module__' and '__qualname__' to find it")
    if '.' in qualname:
        raise ValueError(f"subject must be a top-level function/class")
    # see whether ``subject`` has been bound to its module
    module = sys.modules[module_name]
    if getattr(module, qualname, None) is not subject:
        print('@decorating', qualname)  # @decoraware
    else:
        print('wrapping()', qualname)   # decoraware()
    return subject

This example will merely print how it was applied.

>>> @decoraware
... def foo(): ...
...
@decorating foo
>>> decoraware(foo)
wrapping() foo

The same means can be used to run arbitrary code in each path, though.

In case that multiple decorators are applied, you must decide whether you want the top or bottom subject. For the top-function, the code works unmodified. For the bottom subject, unwrap it using subject = inspect.unwrap(subject) before detection.


The same approach can be used in a more general way on CPython. Using sys._getframe(n).f_locals gives access to the local namespace in which the decorator was applied.

def decoraware(subject):
    """Decorator that is aware whether it was applied using `@deco` syntax"""
    modname, topname = subject.__module__, subject.__name__
    if getattr(sys.modules[modname], topname, None) is subject:
        print('wrapping()', topname, '[top-level]')
    else:
        at_frame = sys._getframe(1)
        if at_frame.f_locals.get(topname) is subject:
            print('wrapping()', topname, '[locals]')
        elif at_frame.f_globals.get(topname) is subject:
            print('wrapping()', topname, '[globals]')
        else:
            print('@decorating', topname)
    return subject

Note that similar to pickle, this approach will fail if the subject's __qualname__/__name__ is tampered with or it is del'ed from its defining namespace.

like image 37
MisterMiyagi Avatar answered Nov 07 '22 04:11

MisterMiyagi