modified change item a little

This commit is contained in:
Jakub Trllo 2023-02-02 11:54:55 +01:00
parent dbda359b9d
commit 0dbb63df94

View file

@ -183,92 +183,169 @@ def prepare_failed_creator_operation_info(
}
class ChangedItem(object):
_EMPTY_VALUE = object()
class TrackChangesItem(object):
"""Helper object to track changes in data.
Has access to full old and new data and will create deep copy of them,
so it is not needed to create copy before passed in.
Can work as a dictionary if old or new value is a dictionary. In
that case received object is another object of 'TrackChangesItem'.
Goal is to be able to get old or new value as was or only changed values
or get information about removed/changed keys, and all of that on
any "dictionary level".
```
# Example of possible usages
old_value = {
"key_1": "value_1",
"key_2": {
"key_sub_1": 1,
"key_sub_2": {
"enabled": True
}
},
"key_3": "value_2"
}
new_value = {
"key_1": "value_1",
"key_2": {
"key_sub_2": {
"enabled": False
},
"key_sub_3": 3
},
"key_3": "value_3"
}
changes = TrackChangesItem(old_value, new_value)
print(changes.changed)
>>> True
print(changes.changed_keys)
>>> {"key_2", "key_3"}
print(changes["key_2"]["key_sub_2"]["enabled"].changed)
>>> True
print(changes["key_2"].removed_keys)
>>> {"key_sub_1"}
print(changes["key_2"].available_keys)
>>> {"key_sub_1", "key_sub_2", "key_sub_3"}
print(changes.new_value == new_value)
>>> True
only_changed_new_values = {
key: changes[key].new_value
for key in changes
}
```
Args:
old_value (Any): Previous value.
new_value (Any): New value.
"""
def __init__(self, old_value, new_value):
self._changed = old_value != new_value
if old_value is _EMPTY_VALUE:
old_value = None
if new_value is _EMPTY_VALUE:
new_value = None
self._old_value = copy.deepcopy(old_value)
self._new_value = copy.deepcopy(new_value)
self._changed = self._old_value != self._new_value
old_is_dict = isinstance(old_value, dict)
new_is_dict = isinstance(new_value, dict)
children = {}
changed_keys = set()
available_keys = set()
new_keys = set()
old_keys = set()
if old_is_dict and new_is_dict:
old_keys = set(old_value.keys())
new_keys = set(new_value.keys())
available_keys = old_keys | new_keys
for key in available_keys:
item = ChangedItem(
old_value.get(key), new_value.get(key)
)
children[key] = item
if item.changed or key not in old_keys or key not in new_keys:
changed_keys.add(key)
self._old_is_dict = isinstance(old_value, dict)
self._new_is_dict = isinstance(new_value, dict)
elif old_is_dict:
old_keys = set(old_value.keys())
available_keys = set(old_keys)
changed_keys = set(available_keys)
for key in available_keys:
children[key] = ChangedItem(old_value.get(key), None)
self._old_keys = None
self._new_keys = None
self._available_keys = None
self._removed_keys = None
elif new_is_dict:
new_keys = set(new_value.keys())
available_keys = set(new_keys)
changed_keys = set(available_keys)
for key in available_keys:
children[key] = ChangedItem(None, new_value.get(key))
self._changed_keys = None
self._changed_keys = changed_keys
self._available_keys = available_keys
self._children = children
self._old_is_dict = old_is_dict
self._new_is_dict = new_is_dict
self._old_keys = old_keys
self._new_keys = new_keys
self._sub_items = None
def __getitem__(self, key):
return self._children[key]
"""Getter looks into subitems if object is dictionary."""
if self._sub_items is None:
self._prepare_sub_items()
return self._sub_items[key]
def __bool__(self):
"""Boolean of object is if old and new value are the same."""
return self._changed
def __iter__(self):
for key in self.changed_keys:
yield key
def get(self, key, default=None):
return self._children.get(key, default)
"""Try to get sub item."""
def keys(self):
return self.changed_keys
if self._sub_items is None:
self._prepare_sub_items()
return self._sub_items.get(key, default)
def items(self):
if not self.is_dict:
yield None, self.changes
else:
for item in self.changes.items():
yield item
@property
def old_value(self):
"""Get copy of old value.
Returns:
Any: Whatever old value was.
"""
return copy.deepcopy(self._old_value)
@property
def new_value(self):
"""Get copy of new value.
Returns:
Any: Whatever new value was.
"""
return copy.deepcopy(self._new_value)
@property
def changed(self):
"""Value changed.
Returns:
bool: If data changed.
"""
return self._changed
@property
def is_dict(self):
"""Object can be used as dictionary.
Returns:
bool: When can be used that way.
"""
return self._old_is_dict or self._new_is_dict
@property
def changes(self):
"""Get changes in raw data.
This method should be used only if 'is_dict' value is 'True'.
Returns:
Dict[str, Tuple[Any, Any]]: Changes are by key in tuple
(<old value>, <new value>). If 'is_dict' is 'False' then
output is always empty dictionary.
"""
output = {}
if not self.is_dict:
return (self.old_value, self.new_value)
return output
old_value = self.old_value
new_value = self.new_value
output = {}
for key in self.changed_keys:
_old = None
_new = None
@ -279,29 +356,135 @@ class ChangedItem(object):
output[key] = (_old, _new)
return output
@property
def changed_keys(self):
return set(self._changed_keys)
@property
def available_keys(self):
return set(self._available_keys)
# Methods/properties that can be used when 'is_dict' is 'True'
@property
def old_keys(self):
"""Keys from old value.
Empty set is returned if old value is not a dict.
Returns:
Set[str]: Keys from old value.
"""
if self._old_keys is None:
self._prepare_keys()
return set(self._old_keys)
@property
def new_keys(self):
"""Keys from new value.
Empty set is returned if old value is not a dict.
Returns:
Set[str]: Keys from new value.
"""
if self._new_keys is None:
self._prepare_keys()
return set(self._new_keys)
@property
def old_value(self):
return copy.deepcopy(self._old_value)
def changed_keys(self):
"""Keys that has changed from old to new value.
Empty set is returned if both old and new value are not a dict.
Returns:
Set[str]: Keys of changed keys.
"""
if self._changed_keys is None:
self._prepare_sub_items()
return set(self._changed_keys)
@property
def new_value(self):
return copy.deepcopy(self._new_value)
def available_keys(self):
"""All keys that are available in old and new value.
Empty set is returned if both old and new value are not a dict.
Output it is Union of 'old_keys' and 'new_keys'.
Returns:
Set[str]: All keys from old and new value.
"""
if self._available_keys is None:
self._prepare_keys()
return set(self._available_keys)
@property
def removed_keys(self):
"""Key that are not available in new value but were in old value.
Returns:
Set[str]: All removed keys.
"""
if self._removed_keys is None:
self._prepare_sub_items()
return set(self._removed_keys)
def _prepare_keys(self):
old_keys = set()
new_keys = set()
if self._old_is_dict and self._new_is_dict:
old_keys = set(self._old_value.keys())
new_keys = set(self._new_value.keys())
elif self._old_is_dict:
old_keys = set(self._old_value.keys())
elif self._new_is_dict:
new_keys = set(self._new_value.keys())
self._old_keys = old_keys
self._new_keys = new_keys
self._available_keys = old_keys | new_keys
self._removed_keys = old_keys - new_keys
def _prepare_sub_items(self):
sub_items = {}
changed_keys = set()
old_keys = self.old_keys
new_keys = self.new_keys
new_value = self.new_value
old_value = self.old_value
if self._old_is_dict and self._new_is_dict:
for key in self.available_keys:
item = TrackChangesItem(
old_value.get(key), new_value.get(key)
)
sub_items[key] = item
if item.changed or key not in old_keys or key not in new_keys:
changed_keys.add(key)
elif self._old_is_dict:
old_keys = set(old_value.keys())
available_keys = set(old_keys)
changed_keys = set(available_keys)
for key in available_keys:
# NOTE Use '_EMPTY_VALUE' because old value could be 'None'
# which would result in "unchanged" item
sub_items[key] = TrackChangesItem(
old_value.get(key), _EMPTY_VALUE
)
elif self._new_is_dict:
new_keys = set(new_value.keys())
available_keys = set(new_keys)
changed_keys = set(available_keys)
for key in available_keys:
# NOTE Use '_EMPTY_VALUE' because new value could be 'None'
# which would result in "unchanged" item
sub_items[key] = TrackChangesItem(
_EMPTY_VALUE, new_value.get(key)
)
self._sub_items = sub_items
self._changed_keys = changed_keys
class InstanceMember:
@ -895,7 +1078,7 @@ class CreatedInstance:
def changes(self):
"""Calculate and return changes."""
return ChangedItem(self.origin_data, self.data_to_store())
return TrackChangesItem(self._orig_data, self.data_to_store())
def mark_as_stored(self):
"""Should be called when instance data are stored.
@ -1519,8 +1702,9 @@ class CreateContext:
def context_data_changes(self):
"""Changes of attributes."""
old_value = copy.deepcopy(self._original_context_data)
return ChangedItem(old_value, self.context_data_to_store())
return TrackChangesItem(
self._original_context_data, self.context_data_to_store()
)
def creator_adds_instance(self, instance):
"""Creator adds new instance to context.