Skip to content

Instantly share code, notes, and snippets.

@boppreh
Created April 1, 2023 22:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save boppreh/9852d69f9ba5afdf718d8d738bd25222 to your computer and use it in GitHub Desktop.
Save boppreh/9852d69f9ba5afdf718d8d738bd25222 to your computer and use it in GitHub Desktop.
Proxy class to track modifications in a Python object and its children
class Proxy:
"""
Wraps an object to keep track of modifications, including to its children.
"""
def __init__(self, obj, modified_flag=None):
# Must use `super().__setattr__` to avoid recursing on itself.
super().__setattr__('_obj', obj)
super().__setattr__('_modified_flag', modified_flag or [False])
@property
def is_modified(self):
""" Returns True if any object in this tree has been modified. """
return self._modified_flag[0]
def _set_modified(self):
self._modified_flag[0] = True
def _wrap_subvalue(self, value):
"""
Given an attribute or index value, decides if it should be returned as-is
(e.g. primitive types), wrapped in another Proxy (e.g. substructures), or
if it's a modifying function call and the respective flag should be set.
"""
if isinstance(value, (int, str, float, bool, bytes)):
return value
elif callable(value):
# List of functions that modify the object.
if value.__qualname__ in ('list.append', 'list.pop', 'list.clear', 'list.extend', 'list.insert', 'list.remove', 'list.sort', 'list.reverse', 'dict.popitem', 'dict.update', 'dict.pop', 'dict.clear'):
self._set_modified()
return value
else:
return Proxy(obj=value, modified_flag=self._modified_flag)
def __getattr__(self, name):
return self._wrap_subvalue(getattr(self._obj, name))
def __setattr__(self, name, value):
self._set_modified()
setattr(self._obj, name, value)
def __getitem__(self, index):
return self._wrap_subvalue(self._obj[index])
def __setitem__(self, index, value):
self._set_modified()
self._obj[index] = value
if __name__ == '__main__':
###
# Example usage
###
from dataclasses import dataclass
@dataclass
class Child:
value: float
@dataclass
class Parent:
"""Class for keeping track of an item in inventory."""
name: str
children: [Child]
def bogus_operation(self) -> float:
return sum(child.value for child in self.children) / len(self.children)
parent = Proxy(Parent('parent name', [Child(value=5), Child(value=4)]))
parent.bogus_operation()
print(parent.is_modified)
parent.children[0].value = 2
parent.children.append(Child(1))
print(parent.is_modified)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment