pythonclassmethodsoverwrite

Intercepting and redirecting calls of a nested class structure in python


I have a very deeply nested class structure spanning multiple files and multiple levels of definitions. You can imagine this as a class describing some hardware system where each hardware component is represented by a class.

class B():

    def __init__(self):
        self.attr_c = 0

    def setup_b(self):
        return "Setup of Class B might fail"

class A():

    def __init__(self):
        self.attr_b = B()

    def setup_a(self):
        return "Setup of Class A might fail"

    def capture_a(self):
        return "Some real captured data"

Class A is instantiated once at the beginning and after that, any of its nested members can be called.

I'd like to create a virtual class (eg: class VirtualA) that replaces the instantiation of Class A at the beginning and allows me to use the same code as before without modification.

The scope of VirtualA is to virtualize the hardware instruments at different nested levels and provide some virtual data.

Requirements are:

  1. Some methods of any nested class should return a specific value (eg success or fail).
  2. Other methods (for example capture_a()) can be overwritten to return some virtualized data
  3. I don't want to replicate the whole structure of call and classes with a virtual counterpart (it would be too onerous). I just want to focus on methods that actually return data that I want to virtualize (eg see capture_a())
  4. Maintain debuggability

example:

# example of class VirtualA definition
class VirtualA():
  
  def capture_a(self):
     return "Some virtual data"

  # TODO: add some minimal code (without replicating the whole nested class) to make the rest of the calls automatically pass


# example of usage of class A
a = A()
a.setup_a()
a.attr_b.setup_b()
a.attr_b.attr_c = 1
print(a.capture_a()) # -> "Some real captured data"

# example of usage of class VirtualA
a = VirtualA()
a.setup_a()
a.attr_b.setup_b()
a.attr_b.attr_c = 1
print(a.capture_a()) # -> "Some virtual data"

Solution

  • Adopted Solution

    The solution that I adopted eventually is to create a Wrapper class to be inherited by my Child classes. This class overrides the __getattr__ method and inspects the code to decide whether the attribute that is attempted to be accessed is a function or a property that is being accessed.

    If the attribute is a property that is being accessed (if a . is found after the attribute name), then I use setattr to create that property and assign it to an instance of the same Wrapper class, in a recursive way.

    If the attribute was a function call (if a ( was found in the code) then I return a wrapper function for which I can specify a return value.

    class Wrapper(object):
    
        output = None
        def __init__(self, output=None):
            self.output = output
    
        def __getattr__(self, name):
            code = str(inspect.stack()[1].code_context)
            is_function = True if code.find(name + '(') != -1 else False
            is_accessed = True if code.find(name + '.') != -1 else False
    
            if is_accessed:
                setattr(self, name, Wrapper(self.output))
                return getattr(self, name)
            if is_function:
                return self.wrapper(self.output)
            else:
                return self.output
    
        @staticmethod
        def wrapper(output):
            def wrapper2(*args, **kwargs):
                return output
            return wrapper2
    
        def __deepcopy__(self, memo):
            print(self.__class__, self.__dict__)
            cls = self.__class__
            result = cls.__new__(cls)
            memo[id(self)] = result
            for k, v in self.__dict__.items():
                setattr(result, k, deepcopy(v, memo)) 
            return result
    

    Note that the Wrapper class can be initialized with the default return value output. I'm also overriding the __deepcopy__ method to enable the deepcopy of the child classes and for this, the output property needs to be a class property (I don't fully understand why this works).

    The way that I use this class is as follows:

    class VirtualB(Wrapper):
        def __init__(self):
            self.output = "B"
    class VirtualC(Wrapper):
        def __init__(self):
            self.output = "C"
    
    
    class VirtualA(VirtualB):
        def __init__(self):
            self.output = "A"
            self.B = VirtualB()
            self.C = VirtualC()
    
        def capture_a(self):
            return "Some virtual data"
    
    
    a = VirtualA()
    print(a.capture_a())
    
    print(a.fun())              # returns A
    print(a.par)                # returns A
    print(a.par.fun())          # returns A
    print(a.par.par.fun())      # returns A
                                        
    print(a.B.fun())            # returns B
    print(a.B.par)              # returns B
    print(a.B.par.fun())        # returns B
    print(a.B.par.par.fun())    # returns B
                                        
    print(a.C.fun())            # returns C
    print(a.C.par)              # returns C
    print(a.C.par.fun())        # returns C
    print(a.C.par.par.fun())    # returns C
    
    d = deepcopy(a)
    

    Where as you can see the only function I got to override is capture_a while the rest of the calls are being recursively handled by the Wrapper class.

    Original Solution - non optimal for my usecase

    A solution that is not optimal requires the instantiation of VirtualB as well (and all the potential additional classes that are instantiated). This is non optimal because it requires to create a Virtual class for every class in the tree.

    In this solution I use __getattr__ to redirect all the calls to methods and a double wrapper to choose the return value of the function.

    class B():
    
        def __init__(self):
            self.attr_c = 0
    
        def setup_b(self):
            return "Setup of Class B might fail"
    
    class A():
    
        def __init__(self):
            self.attr_b = B()
    
        def setup_a(self):
            return "Setup of Class A might fail"
    
        def capture_a(self):
            return "Some real captured data"
    
    class VirtualB():
    
        def __getattr__(self, name):
            return self.wrapper("Setup of VirtualB can't fail")
    
        def wrapper(self,output):
            def wrapper2(*args, **kwargs):
                return output
            return wrapper2
    
    
    class VirtualA():
    
        def __init__(self):
            self.attr_b = VirtualB()
    
        def capture_a(self):
            return "Some virtual data"
    
        def __getattr__(self, name):
            return self.wrapper("Setup of VirtualA can't fail")
    
        def wrapper(self,output):
            def wrapper2(*args, **kwargs):
                return output
            return wrapper2
    
    
    # Usage of A()
    a = A()
    print(a.setup_a())
    print(a.attr_b.setup_b())
    a.attr_b.attr_c = 1
    print(a.attr_b.attr_c)
    print(a.capture_a())
    
    print()
    # Usage of VirtualA()
    # a = A()
    a = VirtualA()
    print(a.setup_a())
    print(a.attr_b.setup_b())
    a.attr_b.attr_c = 1
    print(a.attr_b.attr_c)
    print(a.capture_a())