Introspection to get decorator names on a method?

23,029

Solution 1

If you can change the way you call the decorators from

class Foo(object):
    @many
    @decorators
    @here
    def bar(self):
        pass

to

class Foo(object):
    @register(many,decos,here)
    def bar(self):
        pass

then you could register the decorators this way:

def register(*decorators):
    def register_wrapper(func):
        for deco in decorators[::-1]:
            func=deco(func)
        func._decorators=decorators        
        return func
    return register_wrapper

For example:

def many(f):
    def wrapper(*args,**kwds):
        return f(*args,**kwds)
    return wrapper

decos = here = many

class Foo(object):
    @register(many,decos,here)
    def bar(self):
        pass

foo=Foo()

Here we access the tuple of decorators:

print(foo.bar._decorators)
# (<function many at 0xb76d9d14>, <function decos at 0xb76d9d4c>, <function here at 0xb76d9d84>)

Here we print just the names of the decorators:

print([d.func_name for d in foo.bar._decorators])
# ['many', 'decos', 'here']

Solution 2

I'm surprised that this question is so old and no one has taken the time to add the actual introspective way to do this, so here it is:

The code you want to inspect...

def template(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

baz = template
che = template

class Foo(object):
    @baz
    @che
    def bar(self):
        pass

Now you can inspect the above Foo class with something like this...

import ast
import inspect

def get_decorators(cls):
    target = cls
    decorators = {}

    def visit_FunctionDef(node):
        decorators[node.name] = []
        for n in node.decorator_list:
            name = ''
            if isinstance(n, ast.Call):
                name = n.func.attr if isinstance(n.func, ast.Attribute) else n.func.id
            else:
                name = n.attr if isinstance(n, ast.Attribute) else n.id

            decorators[node.name].append(name)

    node_iter = ast.NodeVisitor()
    node_iter.visit_FunctionDef = visit_FunctionDef
    node_iter.visit(ast.parse(inspect.getsource(target)))
    return decorators

print get_decorators(Foo)

That should print something like this...

{'bar': ['baz', 'che']}

or at least it did when I tested this with Python 2.7.9 real quick :)

Solution 3

I've add the same question. In my unit tests I just wanted to make sure decorators were used by given functions/methods.

The decorators were tested separately so I didn't need to test the common logic for each decorated function, just that the decorators were used.

I finally came up with the following helper function:

import inspect

def get_decorators(function):
    """Returns list of decorators names

    Args:
        function (Callable): decorated method/function

    Return:
        List of decorators as strings

    Example:
        Given:

        @my_decorator
        @another_decorator
        def decorated_function():
            pass

        >>> get_decorators(decorated_function)
        ['@my_decorator', '@another_decorator']

    """
    source = inspect.getsource(function)
    index = source.find("def ")
    return [
        line.strip().split()[0]
        for line in source[:index].strip().splitlines()
        if line.strip()[0] == "@"
    ]

With the list comprehension, it is a bit "dense" but it does the trick and in my case it's a test helper function.

It works if you are intrested only in the decorators names, not potential decorator arguments. If you want to support decorators taking arguments, something like line.strip().split()[0].split("(")[0] could do the trick (untested)

Finally, you can remove the "@" if you'd like by replacing line.strip().split()[0] by line.strip().split()[0][1:]

Solution 4

That's because decorators are "syntactic sugar". Say you have the following decorator:

def MyDecorator(func):
    def transformed(*args):
        print "Calling func " + func.__name__
        func()
    return transformed

And you apply it to a function:

@MyDecorator
def thisFunction():
    print "Hello!"

This is equivalent to:

thisFunction = MyDecorator(thisFunction)

You could embed a "history" into the function object, perhaps, if you're in control of the decorators. I bet there's some other clever way to do this (perhaps by overriding assignment), but I'm not that well-versed in Python unfortunately. :(

Solution 5

As Faisal notes, you could have the decorators themselves attach metadata to the function, but to my knowledge it isn't automatically done.

Share:
23,029
Braedon Wooding
Author by

Braedon Wooding

Updated on May 30, 2020

Comments

  • Braedon Wooding
    Braedon Wooding about 4 years

    I am trying to figure out how to get the names of all decorators on a method. I can already get the method name and docstring, but cannot figure out how to get a list of decorators.

  • Faisal
    Faisal almost 14 years
    This is a great solution. :D It does assume you have access to the code that's assigning the decorators, though...
  • Braedon Wooding
    Braedon Wooding almost 14 years
    Ok this could work, but why can't I just add the code func._whatever='something' into my existing decorator, and test for the value of the _whatever attribute when performing introspection on the method?
  • Faisal
    Faisal almost 14 years
    You can, but then you'll have to dirty every decorator you write with the cross-cutting concern of leaving its tracks behind in the function it modifies.
  • Jmons
    Jmons over 6 years
    Okay, this has problems in python 3 : (at least it seems to, and I'm sure I'm not the best person with knowledge of inspect/ast to comment with that level of certainty); basically I have a the code from what you have above, and inspect.getsource() seems to return with the spaces in front of the def wrapper , which then gives an unexpected indent error on the ast.parse call.
  • Jmons
    Jmons over 6 years
    ALSO when i tried to demonstrate this using hte online run-script tools (which I think script rather then run from a file), I jsut get OSError: source code not available so I suspect there are instances (perhaps also bin runs) where this process won't work. Perhaps it won't work when bin-only runs of python exist?
  • Jaymon
    Jaymon over 6 years
    Which version of python3? I just tested it in python 2.7.13 and python 3.6.4 (which are the versions I have on my computer) and they both worked fine. Also, I'm not sure this will work everywhere, but it's worked everywhere I've needed it. I could definitely see the online run-script having protections for modules like ast and inspect, and probably other things like opening files, since it is, by definition, a more contained environment.
  • Jmons
    Jmons over 6 years
    Thanks for checking: I'll try to have another look but without being able to demonstrate with the nice helpful code sharing it makes it harder. If I can replicated it I'll open it up as a new question and tag you ;)
  • Stanislav Hordiyenko
    Stanislav Hordiyenko almost 4 years
    Works like a charm!
  • ragazzojp
    ragazzojp over 2 years
    This basically works only for your custom @register decorator, we need a more general solution.