Created
April 1, 2023 22:52
-
-
Save boppreh/9852d69f9ba5afdf718d8d738bd25222 to your computer and use it in GitHub Desktop.
Proxy class to track modifications in a Python object and its children
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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