Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python : metaclass + wrapped methods + inheritance = problems

I have a problem in Python, for which I cannot find any clean solution ...

When calling some methods, I want to execute some code before the method execution and after. In order (among many other things) to automatically set and clean a context variable.

In order to achieve this, I have declared the following metaclass :

class MyType(type):
    def __new__(cls, name, bases, attrs):
        #wraps the 'test' method to automate context management and other stuff
        attrs['test'] = cls.other_wrapper(attrs['test'])
        attrs['test'] = cls.context_wrapper(attrs['test'])
        return super(MyType, cls).__new__(cls, name, bases, attrs)

    @classmethod
    def context_wrapper(cls, operation):
        def _manage_context(self, *args, **kwargs):
            #Sets the context to 'blabla' before the execution
            self.context = 'blabla'
            returned = operation(self, *args, **kwargs)
            #Cleans the context after execution
            self.context = None
            return returned
        return _manage_context

    @classmethod
    def other_wrapper(cls, operation):
        def _wrapped(self, *args, **kwargs):
            #DO something with self and *args and **kwargs
            return operation(self, *args, **kwargs)
        return _wrapped

This works like a charm :

class Parent(object):

    __metaclass__ = MyType

    def test(self):
        #Here the context is set:
        print self.context #prints blabla

But as soon as I want to subclass Parent, problems appear, when I call the parent method with super :

class Child(Parent):
    def test(self):
        #Here the context is set too
        print self.context #prints blabla
        super(Child, self).test()
        #But now the context is unset, because Parent.test is also wrapped by _manage_context
        #so this prints 'None', which is not what was expected
        print self.context

I have thought of saving the context before setting it to a new value, but that only solves partially the problem...

Indeed, (hang on, this is hard to explain), the parent method is called, the wrappers are executed, but they receive *args and **kwargs addressed to Parent.test, while self is a Child instance, so self attributes have irrelevant values if I want to challenge them with *args and **kwargs (for example for automated validation purpose), example :

@classmethod
def validation_wrapper(cls, operation):
    def _wrapped(self, *args, **kwargs):
        #Validate the value of a kwarg
        #But if this is executed because we called super(Child, self).test(...
        #`self.some_minimum` will be `Child.some_minimum`, which is irrelevant
        #considering that we called `Parent.test`
        if not kwarg['some_arg'] > self.some_minimum:
            raise ValueError('Validation failed')
        return operation(self, *args, **kwargs)
    return _wrapped

So basically, to solve this problem I see two solutions :

  1. preventing the wrappers to be executed when the method was called with super(Child, self)

  2. having a self that is always of the "right" type

Both solutions seem impossible to me ... Do somebody has an idea on how to solve this ? A suggestion ?

like image 798
sebpiq Avatar asked Nov 14 '22 02:11

sebpiq


1 Answers

Well, can't you just check if the context is already set in _manage_context? Like this:

def _manage_context(self, *args, **kwargs):
    #Sets the context to 'blabla' before the execution
    if self.context is None:
        self.context = 'blabla'
        returned = operation(self, *args, **kwargs)
        #Cleans the context after execution
        self.context = None
        return returned
    else:
        return operation(self, *args, **kwargs)

Also, this should probably be wrapped in a try-catch block, to ensure resetting of the context in case of exceptions.

like image 56
Björn Pollex Avatar answered Dec 18 '22 13:12

Björn Pollex