Search code examples
pythonintrospection

Discriminate between callers inside and outside class hierarchy


I have two classes A and B, where B inherits from A and overrides a property. A is not under my control so I cannot change it.

The code looks as follows:

class A():
    def __init__(self, value):
        self.value = value
    @property
    def value(self):
        return self._value
    @value.setter
    def value(self, value):
        self._value = value

class B(A):
    def __init__(self, value):
        super(B, self).__init__(value)
    @property
    def value(self):
        return super(B, self).value
    @value.setter
    def value(self, value):
        raise AttributeError("can't set attribute")

When I try to call B(1) I obviously get AttributeError: can't set attribute.

I would like to have a different behaviour when value is set from inside class methods

 @value.setter
 def value(self, value):
     if set from inside class hierarchy:
         pass
     else:
         raise AttributeError("can't set attribute")

The module inspect does not seem to give me enough information to do this, except checking against a list of known functions.


Solution

  • You can inspect the stack to determine who called, and whether that it's in the class hierarchy to decide whether or not to allow it:

    import inspect
    
    
    def who_called():
        frame = inspect.stack()[2][0]
        if 'self' not in frame.f_locals:
            return None, None
        cls = frame.f_locals['self'].__class__
        method = frame.f_code.co_name
        return cls, method
    
    class A(object):
        def __init__(self, value):
            self._value = value
    
        @property
        def value(self):
            return self._value
    
        @value.setter
        def value(self, value):
            self._value = value
    
        # Assuming this existed it would also work
        def change_value(self, value):
            self.value = value
    

    Class B now checking:

    class B(A):
        def __init__(self, value):
            super(B, self).__init__(value)
    
        @property
        def value(self):
            return super(B, self).value
    
        @value.setter
        def value(self, value):
            cls, method = who_called()
            if cls in B.__mro__ and method in A.__dict__:
                self._value = value
            else:
                raise AttributeError("can't set attribute")
    

    Proof:

    b = B('does not raise error')
    b.change_value('does not raise error')
    b.value = 'raises error'