Search code examples
pythonintrospectionpython-decorators

Access derived class attribute in base class function decorator


I want to do something like:

class A(Resource):
  @dec(from_file=A.docpath)
  def get(self):
     pass

class B(A):
  docpath = './docs/doc_for_get_b.json'

class C(A):
  docpath = './docs/doc_for_get_c.json'

def dec(*args, **kwargs):
    def inner(f):
       docpath = kwargs.get('from_file')
       f.__kwargs__ = open(path, 'r').read()
       return f
    return inner

The functions that will be called are B.get and C.get, never A.get.

How can I access the custom attribute docpath defined in class B or class C and pass it to the decorator of the get function in class A ?

Current solution: Put the decorator on each derived class ...

class A(Resource):
  def _get(self):
     pass

class B(A):
  @dec(from_file='./docs/doc_for_get_b.json')
  def get(self):
     return self._get()

class C(A)
  @dec(from_file='./docs/doc_for_get_c.json')
  def get(self):
     return self._get()

This works but it's pretty ugly compared to the one-line declaration of the classes in the previous code.


Solution

  • To access a class's attributes inside the decorator is easy:

    def decorator(function):
    
        def inner(self):
            self_type = type(self)
            # self_type is now the class of the instance of the method that this
            # decorator is wrapping
            print('The class attribute docpath is %r' % self_type.docpath)
    
            # need to pass self through because at the point function is
            # decorated it has not been bound to an instance, and so it is just a
            # normal function which takes self as the first argument.
            function(self)
    
        return inner
    
    
    class A:
        docpath = "A's docpath"
    
        @decorator
        def a_method(self):
            print('a_method')
    
    
    class B(A):
        docpath = "B's docpath"
    
    a = A()
    a.a_method()
    
    b = B()
    b.a_method()
    

    In general I've found using multiple levels of decorators, i.e. decorator factory functions that create decorators such as you've used and such as:

    def decorator_factory(**kwargs):
    
        def decorator_function(function):
    
            def wrapper(self):
    
                print('Wrapping function %s with kwargs %s' % (function.__name__, kwargs))
                function(self)
    
            return wrapper
    
        return decorator_function
    
    
    class A:
    
        @decorator_factory(a=2, b=3)
        def do_something(self):
            print('do_something')
    
    a = A()
    a.do_something()
    

    a difficult thing to get right and not easy to comprehend when reading code, so I would err towards using class attributes and generic superclass methods in favour of lots of decorators.

    So in your case, don't pass the file path in as an argument to your decorator factory, but set it as a class attribute on your derived classes, and then write a generic method in your superclass that reads the class attribute from the instance's class.