Reputation: 69
New to Python, trying to solve a problem where I need to implement a class decorator that will keep track of changes to its class and instance attributes. The decorator needs to add a get_change attribute to all class and instance attributes to track their status (INIT, MODIFIED, DELETED) corresponding to initial value, modified value and deleted attribute. For the most part I solved it with the exception of one edge case: modifying, deleting a class attribute.
@change_detection
class Struct(object):
x = 42
def __init__(self, y=0):
self.y = y
a = Struct(11)
a.x == Struct.x == 42 # True
a.y == 11 # True
a.x.get_change == Struct.x.get_change == "INIT" # True
a.y.get_change == "INIT" # True
a.x = 100
a.x.get_change == "MOD" # True
del a.x
a.x.get_change == "DEL" # True
I am stuck with the class attribute like changes:
Struct.x = 10
Struct.x.get_change == "MOD" # False - I don't know how to intercept setting the class attribute
del Struct.x
Struct.x.get_change == "DEL" # False - same as before
So, how do you intercept class setting, deleting of attributes? For the instance level you have __setattr__
and __delattr__
, but what is the equivalent for class level, if any?
Thank you!
Upvotes: 0
Views: 380
Reputation: 69
Thanks to pointers from @Felipe, managed to solve all issues. Again, this is not a practical problem, rather is the code challenge described here.
The idea was to subclass dynamically the decorated class and return proxy objects containing the get_change attribute in addtion to the attributes of the proxied object.
def change_detection(cls):
class NonExistentAttribute(object):
pass
class JNoneMeta(type):
def __subclasscheck__(currentCls, parentCls):
return currentCls == JNone and parentCls == type(None)
class JBoolMeta(type):
def __subclasscheck__(currentCls, parentCls):
return currentCls == JBool and parentCls == type(bool)
class JInt(int):
pass
class JString(str):
pass
class JBool(object, metaclass = JBoolMeta):
def __init__(self, value):
self._value = value
def __bool__(self):
return type(self._value) == type(bool) and self._value
def __eq__(self, value):
return self._value == value
class JNone(object, metaclass = JNoneMeta):
def __bool__(self):
return False
def __eq__(self, value):
return value == None
class Journaled(cls):
@staticmethod
def createAttribute(value, state):
if value == None:
value = JNone()
elif isinstance(value, bool):
value = JBool(value)
elif isinstance(value, int):
value = JInt(value)
elif isinstance(value, str):
value = JString(value)
try: # for functions/methods but allows for lambda
value.get_change = state
except AttributeError:
pass
return value
def __init__(self, *args, **kwargs):
super().__setattr__("__modified__", set())
super().__setattr__("__deleted__", set())
super().__init__(*args, **kwargs)
def __getattribute__(self, name):
try:
v = super().__getattribute__(name)
except AttributeError:
v = NonExistentAttribute()
if not name.startswith("__"):
if name in self.__deleted__:
s = "DEL"
elif name in self.__modified__:
s = "MOD"
else:
s = "INIT" if type(v) != NonExistentAttribute else ""
return Journaled.createAttribute(v, s)
return v
def __setattr__(self, name, value):
if not name.startswith("__") or name not in self.__modified__:
try:
v = self.__getattribute__(name)
if type(v) != NonExistentAttribute and (v != value or typesAreDifferent(type(v), type(value))):
self.__modified__.add(name)
except AttributeError:
pass
super().__setattr__(name, value)
def __delattr__(self, name):
if name in self.__modified__:
self.__modified__.remove(name)
if hasattr(self, name):
self.__deleted__.add(name)
super().__setattr__(name, None)
def typesAreDifferent(subClass, parentClass):
return not (issubclass(subClass, parentClass) or issubclass(parentClass, subClass))
#copy original class attributes to Journaled class
for clsAttr in filter(lambda x: not x.startswith("__"), dir(cls)):
setattr(Journaled, clsAttr, cls.__dict__[clsAttr])
return Journaled
Upvotes: 1
Reputation: 8035
There is no magic method (from my knowledge -- perhaps there is a hack that might work here) that deals with class attributes. You can instead do something like so:
class Verifier:
def __init__(self, obj):
self.obj = obj
self.init = obj.__dict__.copy()
def get_change(self, var):
if var not in self.obj.__dict__:
return "DEL"
elif self.obj.__dict__[var] == self.init[var]:
return "INIT"
elif self.obj.__dict__[var] != self.init[var]:
return "MOD"
class Struct:
x = 42
verifier = Verifier(Struct)
This will allow the following:
Struct.x = 42
print(verifier.get_change("x")) # INIT
Struct.x = 43
print(verifier.get_change("x")) # MOD
del Struct.x
print(verifier.get_change("x")) # DEL
However, note that this will break:
Struct.y = 40
print(verifier.get_change("y"))
Traceback (most recent call last):
File "test.py", line 26, in <module>
print(verifier.get_change("y"))
File "test.py", line 9, in get_change
elif self.obj.__dict__[var] == self.init[var]:
KeyError: 'y'
Since our Verifier
only has access to an older Struct
that did not have a the y
variable.
Edit (3.0): Current progress. Decided to add it here in case you want to check out what I've currently have, as it might help you solve your own issue:
def Proxy(val):
try:
class Obj(type(val)): pass
except:
class Obj(): pass
class Proxy(Obj):
def __init__(self, val):
self.val = val
self.old = val
self.modified = False
self.deleted = False
@property
def get_change(self):
if type(self.val) == type(NONE):
return ""
elif self.deleted:
return "DEL"
elif self.val is not self.old or self.modified or self.val != self.old:
return "MOD"
elif self.val is self.old or self.val == self.old:
return "INIT"
def __getattr__(self, attr):
return getattr(self.val, attr)
def __repr__(self):
return repr(self.val)
def __eq__(self, val):
if self.val == val:
return True
else:
return super(Proxy, self).__eq__(val)
def __bool__(self):
if self.val == None:
return False
else:
return not self.val
return Proxy(val)
def change_detection(cls):
class cls_new(cls):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def __getattribute__(self, attr):
return super(cls_new, self).__getattribute__(attr)
def __getattr__(self, attr):
return Proxy(NONE)
def __setattr__(self, attr, val):
if not attr.startswith("__"):
value = Proxy(val)
# Checks if attr in instance dictionary.
if attr in self.__class__.__dict__:
value.old = self.__class__.__dict__[attr].old
elif attr in self.__dict__:
value.old = self.__dict__[attr].old
if self.__dict__[attr] != val and val is None:
value.modified = True
else:
value = val
super(self.__class__, self).__setattr__(attr, value)
def __delattr__(self, attr):
if attr in self.__class__.__dict__:
self.__class__.__dict__[attr].val = None
self.__class__.__dict__[attr].deleted = True
if attr in self.__dict__:
self.__dict__[attr].val = None
self.__dict__[attr].deleted = True
try:
# Copies class attributes to cls_new.__class__.__dict__ as Proxy objects.
for attr in dir(cls()):
if not callable(getattr(cls(), attr)) and not attr.startswith("__") and attr in cls.__dict__:
setattr(cls_new, attr, Proxy(cls.__dict__[attr]))
for attr in dir(cls):
if not attr.startswith("__") and callable(cls.__dict__[attr]) and cls.__dict__[attr].__name__ == (lambda: 0).__name__:
setattr(cls_new, attr, Proxy(cls.__dict__[attr]))
except:
pass
return cls_new
Upvotes: 1