Wednesday 6 July 2022

Intercept instance access

As of lately I've been reviewing some Groovy code, mainly to remember how metaclasses are used in Groovy (they work quite differently from Python). Both instances and classes have a metaclass property, and among other things they can be used to intercept access to a specific instance or to all instances of a class. You can check it here



class InterceptionThroughMetaClassTest extends GroovyTestCase {

    void testPOJOMetaClassInterception() {
        String invoking = 'ha'
        invoking.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert invoking.length() == 'invoked'
        assert invoking.someMethod() == 'invoked'
    }

    void testPOGOMetaClassInterception() {
        Entity entity = new Entity('Hello')
        entity.metaClass.invokeMethod = { String name, Object args ->
            'invoked'
        }

        assert entity.build(new Object()) == 'invoked'
        assert entity.someMethod() == 'invoked'
    }
}


In Python we don't use metaclasses for that. We define a __getattribute__ method (you can check this previous post) in one class and this way we intercept the access to all instances of that class. So. what if we wanted to intercept the access only to one specific instance? Defining __getattribute__ at the instance level won't work.

But given Python's versatility, there's an easy trick to achieve just that. Given an instance that we want to intercept we can create a new class that inherits from that instance's class. Add a __getattribute__ method to that class, and then change the class of the instance from the original class to the derived class that includes the __getattribute__ method. Nice!


# In Groovy we can add interception to an instance (not to a whole class) by means of adding it to the instance metaclass
# we can do something like that in Python by creating a new class that inherits the original class of the instance, and changing the class of the instance
# in that new class we implement __getattribute__

class Person:
    def __init__(self, name):
        self.name = name

    def say_hi(self, person):
        #print("inside say_hi")
        return f"Hi person, I'm {self.name}"


def add_interception_to_instance(obj, interceptor_fn):
    class Interceptor_class(obj.__class__):
        def __getattribute__(self, name):
            interceptor_fn(self, name)
            return object.__getattribute__(self, name)
    obj.__class__ = Interceptor_class

p1 = Person("Xuan")
print(p1.say_hi("Francois"))
# Hi person, I'm Xuan

def logger_interceptor_fn(obj, attr):
    print(f"intercepting access to {attr}")

add_interception_to_instance(p1, logger_interceptor_fn)
print(p1.say_hi("Francois"))
#interception happens:
# intercepting access to say_hi
# intercepting access to name
# Hi person, I'm Xuan
print(isinstance(p1, Person))
# True

# other instances of Person are not intercepted
p2 = Person("Iyan")
print(p2.say_hi("Francois"))
# Hi person, I'm Iyan

I've uploaded it to a gist

No comments:

Post a Comment