Introspection to get decorator names on a method?
Asked Answered
S

10

43

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.

Schmeltzer answered 12/7, 2010 at 20:30 Comment(7)
This seems needless. You have the source. What's wrong with reading the source?Rawlins
@S.Lott: couldn't you answer the same way about any question involving introspection? And yet introspection is useful.Supplement
@S.Lott: Nothing is wrong with reading the source, much the same as nothing is wrong with reading the contents of a database directly instead of using views or scripting, unless I want automation. I use decorators for authentication and I am generating reports with different views to show what user groups have access to which resources. So I need programmatic access to the source, the same as I need programmatic access to a data source.Schmeltzer
@Ned Batchelder: I'm not "answering" -- at least I don't think I am. I'm asking what the use case is. Introspection is something the lawyers call an "attractive nuisance". I don't get the use case for this example of introspection. The question is too short and thin on details.Rawlins
That's not a very helpful clarification. Can you provide some use case and some code?Rawlins
A little late to the game here, but a very good reason for this is documentation. In Django, for example, there is a decorator that a lot of people use: @require_http_methods(['GET', 'POST', ]). In addition to printing out a docstring for a method, it could be nice to print out the http_methods that a method implements.Callum
Being even more late, but another use case is unit testing api endpoints. If you have authorization checks as decorators you don't need to setup every test for every endpoint for those. Just test the decorators separately and check if the actually tested endpoint is decorated with that or not. Naturally if you have fake authorization decorators with the same name declared, you can cheat yourself into believing the endpoint is safe. So this requires some clean design behavior beforehand.Haricot
M
35

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']
Medellin answered 12/7, 2010 at 20:53 Comment(4)
This is a great solution. :D It does assume you have access to the code that's assigning the decorators, though...Bookworm
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?Schmeltzer
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.Bookworm
This basically works only for your custom @register decorator, we need a more general solution.End
M
53

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 :)

Megrim answered 3/7, 2015 at 1:49 Comment(6)
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.Amendatory
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?Amendatory
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.Megrim
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 ;)Amendatory
How would something in decorator_list not an instance of ast.Call? Is it necessary to check this?Laird
@FishMonitor This is a perfect example of why it's great to comment the code with why something was done because I'm not sure why I added that check anymore. You can definitely remove the ast.Call check in your own code and see if you still get the desired resultsMegrim
M
35

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']
Medellin answered 12/7, 2010 at 20:53 Comment(4)
This is a great solution. :D It does assume you have access to the code that's assigning the decorators, though...Bookworm
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?Schmeltzer
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.Bookworm
This basically works only for your custom @register decorator, we need a more general solution.End
T
14

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:]

Trevor answered 29/5, 2020 at 14:13 Comment(0)
B
1

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. :(

Bookworm answered 12/7, 2010 at 20:41 Comment(0)
C
1

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

Calculator answered 12/7, 2010 at 20:51 Comment(0)
R
1

ast can be used to parse source code.

import ast


with open('./tmp1.py') as f:
    code_text = f.read()


parsed = ast.parse(code_text)

Here's my attempt at parsing the parsed object for decorators. This implementation recursively searches for applied decorators. It'll capture usage of @ on functions, classes and methods as well as when they're nested.

import ast


def flatten(decorator_list):
    for d in decorator_list:
        try:
            yield d.id
        except AttributeError:
            yield d.func.id


def parse_decorables(body, indent=0):
    for node in body:
        if isinstance(node, (ast.FunctionDef, ast.ClassDef)):
            print('    '*indent, node.name, '-', *flatten(node.decorator_list))
            parse_decorables(node.body, indent+1)


with open('./tmp1.py') as f:
    code_text = f.read()


parsed = ast.parse(code_text)
parse_decorables(parsed.body)

There's a few things to bear in mind when considering this question. A decorator may just add an attribute to the function and return it. At that point, there's no association between the decorator and the original function. Therefore, there's no way to dynamically inspect a function and ensure we're seeing all the decorators that were applied to it.

So we go to this ast solution and inspect the code. However, it has it's own limitations. An imported decorator may apply more decorators to the function, this will not catch that. If you 'apply a decorator' the way it was done before @ was introduced (f = deco(f)), this will not catch that. So, really we're just capturing uses of @ in the source file, which may be suitable for your application.

Reservation answered 24/3, 2023 at 6:54 Comment(0)
B
0

It is impossible to do in a general way, because

@foo
def bar ...

is exactly the same as

def bar ...
bar = foo (bar)

You may do it in certain special cases, like probably @staticmethod by analyzing function objects, but not better than that.

Bertsche answered 12/7, 2010 at 20:39 Comment(0)
K
0

If you don't need to know the decorator names from anywhere in your code, but for example from within the method itself, here is a different solution which uses a class decorator:

from inspect import currentframe
  
class MyDecorator(object):
    def __init__(self, *decorator_names):
        self.decorator_names = decorator_names

    def __call__(self, func):

        def decorator_wrapper(*args, **kwargs):
            decorator_results = []
            for decorator_name in self.decorator_names:
                try:
                    decorator = MyDecorator.__dict__[decorator_name]
                except KeyError:
                    # raise some error if you like
                    pass
                else:
                    decorator_results.append(decorator(self, func, *args, **kwargs))

            result = return_sth_from(decorator_results)
            return result

        return decorator_wrapper

    def some_decorator(self, func, *args, **kwargs):
        print("I am some decorator.")
        return func(*args, **kwargs)

    def another_decorator(self, func, *args, **kwargs):
        print("I am another decorator.")
        return func(*args, **kwargs)
   
@MyDecorator("some_decorator")
def main():
    decorator_frame = currentframe().f_back
    print(decorator_frame.f_locals['self'].decorator_names)

which prints:

('some_decorator',)

In case you are unfamiliar with class decorators, you can read more on that here.

Kedah answered 14/5, 2024 at 14:23 Comment(0)
G
-1

That's not possible in my opinion. A decorator is not some kind of attribute or meta data of a method. A decorator is a convenient syntax for replacing a function with the result of a function call. See http://docs.python.org/whatsnew/2.4.html?highlight=decorators#pep-318-decorators-for-functions-and-methods for more details.

Giffy answered 12/7, 2010 at 20:38 Comment(0)
L
-1

You can't but even worse is there exists libraries to help hide the fact that you have decorated a function to begin with. See Functools or the decorator library (@decorator if I could find it) for more information.

Lubric answered 12/7, 2010 at 21:22 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.