Search code examples
pythonpython-decoratorsintrospection

Get decorated class from its name in the decorator?


I decorated some methods with @bot_thinking, which stores some information about the decorated method in the functions attribute. One piece of information is 'class_name', but my program needs the class type as a variable, e.g. RandomBot. I would like to get this class.

Here is some sample code:

class DepthPrunedMinimaxAgent(Agent):
    @bot_thinking(associated_name="minimax profondeur")
    def select_move(self, game_state: GameState):

Above is the decorated part of the code.

The decorator:

functions = {}

def bot_thinking(associated_name, active=True):
    def _(func):
        if active:
            class_name = func.__qualname__.rsplit('.')[-2]
            import sys
            # class_name_2=getattr(sys.modules[__name__], class_name)
            # module=importlib.import_module('sources.agent')

            functions[associated_name] = (associated_name, class_name,
                                          globals()[class_name], func)
        else:
            functions.pop(associated_name)

    return _

bot_thinking isn't a real decorator, it's a decorator factory. From the func function, I get the class_name, but I can't use the accepted answer by @m.kocikowski, to find the correct class because this class is decorated, so it already imports the annotation module, so importing from the module of the annotation the annotated module would result in a cyclic import, which python does not seem to permit.

Do you see a method to get the class from its name?

ps: ps: to be clearer : the annotation part of the code need an import to the annotated classes(to retrieve the class from its name), which also need an importation of the annotation (for the annotation to work).


Solution

  • You can do what you want if you use a descriptor class, rather than a function, as the decorator, at least if you're using Python 3.6 or newer. That's because there's a new method added to the descriptor protocol, __set_name__. It gets called when the descriptor object is saved as a class variable. While most descriptors will use it to record the name they're being saved as, you can use it to get the class you're in.

    You do need to make your decorator object wrap the real function (implementing calling and descriptor lookup methods), rather than being able to return the unmodified function you were decorating. Here's my attempt at a quick and dirty implementation. I don't really understand what you're doing with functions, so I may not have put the right data in it, but it should be close enough to get the idea across (owner is the class the method stored in).

    functions = {}
    
    def bot_thinking(associated_name, active=True):
        class decorator:
            def __init__(self, func):
                self.func = func
    
            def __set_name__(self, owner, name):
                if active:
                     functions[associated_name] = (associated_name, owner.__name__,
                                                   owner, self.func)
                else:
                     functions.pop(associated_name)
    
            def __get__(self, obj, owner):
                return self.func.__get__(obj, owner)
    
            def __call__(self, *args, **kwargs):
                return self.func(*args, **kwargs)
    
        return decorator