From 0dbb63df944d81d7318f0ea503f00cf7cca1f48d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 2 Feb 2023 11:54:55 +0100 Subject: [PATCH] modified change item a little --- openpype/pipeline/create/context.py | 324 ++++++++++++++++++++++------ 1 file changed, 254 insertions(+), 70 deletions(-) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 59314b9236..52e43f500e 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -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 + (, ). 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.