pythonmetaprogramming

Method decorators which "tag" method - prevent overwriting by other decorators


I'm investigating the pattern whereby you have a method decorator which annotates the method in some way, and then once the class is defined it looks through its methods, finds the annotated methods, and registers or processes them in some way. e.g.

def class_decorator(cls):
   for name, method in cls.__dict__.iteritems():
        if hasattr(method, "use_class"):
            # do something with the method and class
            print name, cls
   return cls

def method_decorator(view):
    # mark the method as something that requires view's class
    view.use_class = True
    return view

@class_decorator
class ModelA(object):
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

(From here: https://stackoverflow.com/a/2367605/1256529)

I wondered if anyone has a solution to the problem of multiple decorators. For example, this example works fine in isolation, but if I do the following, it will break because the use_class annotation will be hidden.

def hiding_decorator(fn):
    def wrapper(*args, **kwargs):
        print("do some logging etc.")
        return fn(*args, **kwargs)
    return wrapper

@class_decorator
class ModelA(object):
    @hiding_decorator
    @method_decorator
    def a_method(self):
        # do some stuff
        pass

Does anyone have a reliable and user-friendly way around this problem?

Non-optimal solutions I can think of:

None of these are particularly attractive.


Solution

  • Since a "tag" is always applied on a function object itself, it is always going to be susceptible to obstruction by an unaware wrapper function.

    To achieve a similar usage you can implement the behavior with a registry pattern instead by adding method names to a list with a method decorator so that the class decorator can obtain the final decorated function object with getattr:

    class Registry:
        def __init__(self):
            self.names = []
    
        def register(self, func):
            self.names.append(func.__name__)
            return func
    
        def apply(self, cls):
            for name in self.names:
                print(cls, name)
                print(getattr(cls, name))
    
    registry = Registry()
    @registry.apply
    class ModelA(object):
        @hiding_decorator
        @registry.register
        def a_method(self):
            pass
    

    This outputs something like:

    <class '__main__.ModelA'> a_method
    <function hiding_decorator.<locals>.wrapper at 0x1551795d67a0>
    

    Demo: https://ideone.com/R43Izy