0

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

martineau
  • 119,623
  • 25
  • 170
  • 301
lolveley
  • 1,659
  • 2
  • 18
  • 34
  • To make sure I understand: the idea is that when you decorate `select_move` as in the first example, you want the decorator to be able to look up the containing `DepthPrunedMinimaxAgent` (and you're trying to determine its string name and look that up in `globals()`, which isn't working)? Okay... what will you do if the decorated function is *not* inside a class? – Karl Knechtel Sep 16 '19 at 21:08
  • thanks for watching. it will not happen, because the annotations concern specific methods of Agent's subclasses. – lolveley Sep 16 '19 at 21:19

2 Answers2

1

The problem is the class hasn't been defined yet when the bot_thinking() decorator factory (and decorator itself) are executing. The only workaround I can think of would be to patch things up after the class is defined, as illustrated below:

from pprint import pprint, pformat

functions = {}

def bot_thinking(associated_name, active=True):
    def _(func):
        if active:
            class_name = func.__qualname__.split(".")[-2]
            functions[associated_name] = (associated_name, class_name, class_name, func)
        else:
            functions.pop(associated_name, None)

        return func # Decorators must return a callable.

    return _


class Agent: pass
class GameState: pass

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

# After class is defined, update data put into functions dictionary.
for associated_name, info in functions.items():
    functions[associated_name] = (info[0], info[1], globals()[info[2]], info[3])

pprint(functions)

Output:

{'minimax profondeur': ('minimax profondeur',
                        'DepthPrunedMinimaxAgent',
                        <class '__main__.DepthPrunedMinimaxAgent'>,
                        <function DepthPrunedMinimaxAgent.select_move at 0x00F158A0>)}
martineau
  • 119,623
  • 25
  • 170
  • 301
  • well, I understand the answer, it's possible but a little annoying because the purpose of @bot_thinking is to get rid of all heavy and dangerous meta code, all that is not bot declaration or bots exploitation. you talked about other answers, which are they? – lolveley Sep 16 '19 at 21:57
  • lolveley: It may not be the only workaround, just the only one I can think of — which of course doesn't necessarily mean that there aren't any others. The core problem is that `class` definitions are executable in Python, which makes it impossible (AFAIK) for them to be referenced before that process completes). – martineau Sep 16 '19 at 22:04
1

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
Blckknght
  • 100,903
  • 11
  • 120
  • 169