I can think of one solution, it isn't perfect but it's probably a start. We can capture instance attributes accesses via __getattribute__
and __setattribute__
in a class that will inherit from the decorated class:
import re
dunder_pattern = re.compile("__.*__")
protected_pattern = re.compile("_.*")
def is_hidden(attr_name):
return dunder_pattern.match(attr_name) or protected_pattern.match(attr_name)
def attach_proxy(function=None):
function = function or (lambda *a: None)
def decorator(decorated_class):
class Proxy(decorated_class):
def __init__(self, *args, **kwargs):
function("init", args, kwargs)
super().__init__(*args, **kwargs)
def __getattribute__(self, name):
if not is_hidden(name):
function("acces", name)
return object.__getattribute__(self, name)
def __getattr__(self, name):
if not is_hidden(name):
function("acces*", name)
return object.__getattr__(self, name)
def __setattribute__(self, name, value):
if not is_hidden(name):
function("set", name, value)
return object.__setattribute__(self, name, value)
def __setattr__(self, name, value):
if not is_hidden(name):
function("set*", name, value)
return object.__setattr__(self, name, value)
return Proxy
return decorator
Which you can then use to decorate your class:
@attach_proxy(print)
class A:
x = 1
def __init__(self, y, msg="hello"):
self.y = y
@classmethod
def foo(cls):
print(cls.x)
def bar(self):
print(self.y)
Which will result in the following:
>>> a = A(10, msg="test")
init (10,) {'msg': 'test'}
set* y 10
>>> a.bar()
acces bar
acces y
10
>>> a.foo() # access to x is not captured
acces foo
1
>>> y = a.y
acces y
>>> x = A.x # access to x is not captured
>>> a.y = 3e5
set* y 300000.0
Problems:
Class attributes access are not captured (would need a metaclass for that but I don't see a way to do on the fly).
Type A
is hidden (behind the type Proxy
), this is probably simpler to solve:
>>> A
__main__.attach_proxy.<locals>.decorator.<locals>.Proxy
On the other hand that's not necessarily a problem as this will work as expected:
>>> a = A(10, msg="test")
>>> isinstance(a, A)
True
Edit note that I don't pass instances to function
calls, but that would actually be a good idea, replacing calls from function("acces", name)
to function("acces", self, name)
. That would allow to make much more funny things with your decorator.