aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/deepdiff/model.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/deepdiff/model.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/deepdiff/model.py')
-rw-r--r--.venv/lib/python3.12/site-packages/deepdiff/model.py974
1 files changed, 974 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/deepdiff/model.py b/.venv/lib/python3.12/site-packages/deepdiff/model.py
new file mode 100644
index 00000000..41dd7517
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/deepdiff/model.py
@@ -0,0 +1,974 @@
+import logging
+from collections.abc import Mapping
+from copy import copy
+from deepdiff.helper import (
+ RemapDict, strings, notpresent, get_type, numpy_numbers, np, literal_eval_extended,
+ dict_, SetOrdered)
+from deepdiff.path import stringify_element
+
+logger = logging.getLogger(__name__)
+
+FORCE_DEFAULT = 'fake'
+UP_DOWN = {'up': 'down', 'down': 'up'}
+
+REPORT_KEYS = {
+ "type_changes",
+ "dictionary_item_added",
+ "dictionary_item_removed",
+ "values_changed",
+ "unprocessed",
+ "iterable_item_added",
+ "iterable_item_removed",
+ "iterable_item_moved",
+ "attribute_added",
+ "attribute_removed",
+ "set_item_removed",
+ "set_item_added",
+ "repetition_change",
+}
+
+CUSTOM_FIELD = "__internal:custom:extra_info"
+
+
+class DoesNotExist(Exception):
+ pass
+
+
+class ResultDict(RemapDict):
+
+ def remove_empty_keys(self):
+ """
+ Remove empty keys from this object. Should always be called after the result is final.
+ :return:
+ """
+ empty_keys = [k for k, v in self.items() if not isinstance(v, (int)) and not v]
+
+ for k in empty_keys:
+ del self[k]
+
+
+class TreeResult(ResultDict):
+ def __init__(self):
+ for key in REPORT_KEYS:
+ self[key] = SetOrdered()
+
+ def mutual_add_removes_to_become_value_changes(self):
+ """
+ There might be the same paths reported in the results as removed and added.
+ In such cases they should be reported as value_changes.
+
+ Note that this function mutates the tree in ways that causes issues when report_repetition=True
+ and should be avoided in that case.
+
+ This function should only be run on the Tree Result.
+ """
+ iterable_item_added = self.get('iterable_item_added')
+ iterable_item_removed = self.get('iterable_item_removed')
+ if iterable_item_added is not None and iterable_item_removed is not None:
+ added_paths = {i.path(): i for i in iterable_item_added}
+ removed_paths = {i.path(): i for i in iterable_item_removed}
+ mutual_paths = set(added_paths) & set(removed_paths)
+
+ if mutual_paths and 'values_changed' not in self or self['values_changed'] is None:
+ self['values_changed'] = SetOrdered()
+ for path in mutual_paths:
+ level_before = removed_paths[path]
+ iterable_item_removed.remove(level_before)
+ level_after = added_paths[path]
+ iterable_item_added.remove(level_after)
+ level_before.t2 = level_after.t2
+ self['values_changed'].add(level_before) # type: ignore
+ level_before.report_type = 'values_changed'
+ if 'iterable_item_removed' in self and not iterable_item_removed:
+ del self['iterable_item_removed']
+ if 'iterable_item_added' in self and not iterable_item_added:
+ del self['iterable_item_added']
+
+ def __getitem__(self, item):
+ if item not in self:
+ self[item] = SetOrdered()
+ return self.get(item)
+
+ def __len__(self):
+ length = 0
+ for value in self.values():
+ if isinstance(value, SetOrdered):
+ length += len(value)
+ elif isinstance(value, int):
+ length += 1
+ return length
+
+
+class TextResult(ResultDict):
+ ADD_QUOTES_TO_STRINGS = True
+
+ def __init__(self, tree_results=None, verbose_level=1):
+ self.verbose_level = verbose_level
+ # TODO: centralize keys
+ self.update({
+ "type_changes": dict_(),
+ "dictionary_item_added": self.__set_or_dict(),
+ "dictionary_item_removed": self.__set_or_dict(),
+ "values_changed": dict_(),
+ "unprocessed": [],
+ "iterable_item_added": dict_(),
+ "iterable_item_removed": dict_(),
+ "iterable_item_moved": dict_(),
+ "attribute_added": self.__set_or_dict(),
+ "attribute_removed": self.__set_or_dict(),
+ "set_item_removed": SetOrdered(),
+ "set_item_added": SetOrdered(),
+ "repetition_change": dict_()
+ })
+
+ if tree_results:
+ self._from_tree_results(tree_results)
+
+ def __set_or_dict(self):
+ return {} if self.verbose_level >= 2 else SetOrdered()
+
+ def _from_tree_results(self, tree):
+ """
+ Populate this object by parsing an existing reference-style result dictionary.
+ :param tree: A TreeResult
+ :return:
+ """
+ self._from_tree_type_changes(tree)
+ self._from_tree_default(tree, 'dictionary_item_added')
+ self._from_tree_default(tree, 'dictionary_item_removed')
+ self._from_tree_value_changed(tree)
+ self._from_tree_unprocessed(tree)
+ self._from_tree_default(tree, 'iterable_item_added')
+ self._from_tree_default(tree, 'iterable_item_removed')
+ self._from_tree_iterable_item_moved(tree)
+ self._from_tree_default(tree, 'attribute_added')
+ self._from_tree_default(tree, 'attribute_removed')
+ self._from_tree_set_item_removed(tree)
+ self._from_tree_set_item_added(tree)
+ self._from_tree_repetition_change(tree)
+ self._from_tree_deep_distance(tree)
+ self._from_tree_custom_results(tree)
+
+ def _from_tree_default(self, tree, report_type, ignore_if_in_iterable_opcodes=False):
+ if report_type in tree:
+
+ for change in tree[report_type]: # report each change
+ # When we convert from diff to delta result, we care more about opcodes than iterable_item_added or removed
+ if (
+ ignore_if_in_iterable_opcodes
+ and report_type in {"iterable_item_added", "iterable_item_removed"}
+ and change.up.path(force=FORCE_DEFAULT) in self["_iterable_opcodes"]
+ ):
+ continue
+ # determine change direction (added or removed)
+ # Report t2 (the new one) whenever possible.
+ # In cases where t2 doesn't exist (i.e. stuff removed), report t1.
+ if change.t2 is not notpresent:
+ item = change.t2
+ else:
+ item = change.t1
+
+ # do the reporting
+ report = self[report_type]
+ if isinstance(report, SetOrdered):
+ report.add(change.path(force=FORCE_DEFAULT))
+ elif isinstance(report, dict):
+ report[change.path(force=FORCE_DEFAULT)] = item
+ elif isinstance(report, list): # pragma: no cover
+ # we don't actually have any of those right now, but just in case
+ report.append(change.path(force=FORCE_DEFAULT))
+ else: # pragma: no cover
+ # should never happen
+ raise TypeError("Cannot handle {} report container type.".
+ format(report))
+
+ def _from_tree_type_changes(self, tree):
+ if 'type_changes' in tree:
+ for change in tree['type_changes']:
+ path = change.path(force=FORCE_DEFAULT)
+ if type(change.t1) is type:
+ include_values = False
+ old_type = change.t1
+ new_type = change.t2
+ else:
+ include_values = True
+ old_type = get_type(change.t1)
+ new_type = get_type(change.t2)
+ remap_dict = RemapDict({
+ 'old_type': old_type,
+ 'new_type': new_type,
+ })
+ if self.verbose_level > 1:
+ new_path = change.path(use_t2=True, force=FORCE_DEFAULT)
+ if path != new_path:
+ remap_dict['new_path'] = new_path
+ self['type_changes'][path] = remap_dict
+ if self.verbose_level and include_values:
+ remap_dict.update(old_value=change.t1, new_value=change.t2)
+
+ def _from_tree_value_changed(self, tree):
+ if 'values_changed' in tree and self.verbose_level > 0:
+ for change in tree['values_changed']:
+ path = change.path(force=FORCE_DEFAULT)
+ the_changed = {'new_value': change.t2, 'old_value': change.t1}
+ if self.verbose_level > 1:
+ new_path = change.path(use_t2=True, force=FORCE_DEFAULT)
+ if path != new_path:
+ the_changed['new_path'] = new_path
+ self['values_changed'][path] = the_changed
+ if 'diff' in change.additional:
+ the_changed.update({'diff': change.additional['diff']})
+
+ def _from_tree_iterable_item_moved(self, tree):
+ if 'iterable_item_moved' in tree and self.verbose_level > 1:
+ for change in tree['iterable_item_moved']:
+ the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2}
+ self['iterable_item_moved'][change.path(
+ force=FORCE_DEFAULT)] = the_changed
+
+ def _from_tree_unprocessed(self, tree):
+ if 'unprocessed' in tree:
+ for change in tree['unprocessed']:
+ self['unprocessed'].append("{}: {} and {}".format(change.path(
+ force=FORCE_DEFAULT), change.t1, change.t2))
+
+ def _from_tree_set_item_added_or_removed(self, tree, key):
+ if key in tree:
+ set_item_info = self[key]
+ is_dict = isinstance(set_item_info, Mapping)
+ for change in tree[key]:
+ path = change.up.path(
+ ) # we want't the set's path, the added item is not directly accessible
+ item = change.t2 if key == 'set_item_added' else change.t1
+ if self.ADD_QUOTES_TO_STRINGS and isinstance(item, strings):
+ item = "'%s'" % item
+ if is_dict:
+ if path not in set_item_info:
+ set_item_info[path] = set() # type: ignore
+ set_item_info[path].add(item)
+ else:
+ set_item_info.add("{}[{}]".format(path, str(item)))
+ # this syntax is rather peculiar, but it's DeepDiff 2.x compatible)
+
+ def _from_tree_set_item_added(self, tree):
+ self._from_tree_set_item_added_or_removed(tree, key='set_item_added')
+
+ def _from_tree_set_item_removed(self, tree):
+ self._from_tree_set_item_added_or_removed(tree, key='set_item_removed')
+
+ def _from_tree_repetition_change(self, tree):
+ if 'repetition_change' in tree:
+ for change in tree['repetition_change']:
+ path = change.path(force=FORCE_DEFAULT)
+ self['repetition_change'][path] = RemapDict(
+ change.additional['repetition']
+ )
+ self['repetition_change'][path]['value'] = change.t1
+
+ def _from_tree_deep_distance(self, tree):
+ if 'deep_distance' in tree:
+ self['deep_distance'] = tree['deep_distance']
+
+ def _from_tree_custom_results(self, tree):
+ for k, _level_list in tree.items():
+ if k not in REPORT_KEYS:
+ if not isinstance(_level_list, SetOrdered):
+ continue
+
+ # if len(_level_list) == 0:
+ # continue
+ #
+ # if not isinstance(_level_list[0], DiffLevel):
+ # continue
+
+ # _level_list is a list of DiffLevel
+ _custom_dict = {}
+ for _level in _level_list:
+ _custom_dict[_level.path(
+ force=FORCE_DEFAULT)] = _level.additional.get(CUSTOM_FIELD, {})
+ self[k] = _custom_dict
+
+
+class DeltaResult(TextResult):
+ ADD_QUOTES_TO_STRINGS = False
+
+ def __init__(self, tree_results=None, ignore_order=None, always_include_values=False, _iterable_opcodes=None):
+ self.ignore_order = ignore_order
+ self.always_include_values = always_include_values
+
+ self.update({
+ "type_changes": dict_(),
+ "dictionary_item_added": dict_(),
+ "dictionary_item_removed": dict_(),
+ "values_changed": dict_(),
+ "iterable_item_added": dict_(),
+ "iterable_item_removed": dict_(),
+ "iterable_item_moved": dict_(),
+ "attribute_added": dict_(),
+ "attribute_removed": dict_(),
+ "set_item_removed": dict_(),
+ "set_item_added": dict_(),
+ "iterable_items_added_at_indexes": dict_(),
+ "iterable_items_removed_at_indexes": dict_(),
+ "_iterable_opcodes": _iterable_opcodes or {},
+ })
+
+ if tree_results:
+ self._from_tree_results(tree_results)
+
+ def _from_tree_results(self, tree):
+ """
+ Populate this object by parsing an existing reference-style result dictionary.
+ :param tree: A TreeResult
+ :return:
+ """
+ self._from_tree_type_changes(tree)
+ self._from_tree_default(tree, 'dictionary_item_added')
+ self._from_tree_default(tree, 'dictionary_item_removed')
+ self._from_tree_value_changed(tree)
+ if self.ignore_order:
+ self._from_tree_iterable_item_added_or_removed(
+ tree, 'iterable_item_added', delta_report_key='iterable_items_added_at_indexes')
+ self._from_tree_iterable_item_added_or_removed(
+ tree, 'iterable_item_removed', delta_report_key='iterable_items_removed_at_indexes')
+ else:
+ self._from_tree_default(tree, 'iterable_item_added', ignore_if_in_iterable_opcodes=True)
+ self._from_tree_default(tree, 'iterable_item_removed', ignore_if_in_iterable_opcodes=True)
+ self._from_tree_iterable_item_moved(tree)
+ self._from_tree_default(tree, 'attribute_added')
+ self._from_tree_default(tree, 'attribute_removed')
+ self._from_tree_set_item_removed(tree)
+ self._from_tree_set_item_added(tree)
+ self._from_tree_repetition_change(tree)
+
+ def _from_tree_iterable_item_added_or_removed(self, tree, report_type, delta_report_key):
+ if report_type in tree:
+ for change in tree[report_type]: # report each change
+ # determine change direction (added or removed)
+ # Report t2 (the new one) whenever possible.
+ # In cases where t2 doesn't exist (i.e. stuff removed), report t1.
+ if change.t2 is not notpresent:
+ item = change.t2
+ else:
+ item = change.t1
+
+ # do the reporting
+ path, param, _ = change.path(force=FORCE_DEFAULT, get_parent_too=True)
+ try:
+ iterable_items_added_at_indexes = self[delta_report_key][path]
+ except KeyError:
+ iterable_items_added_at_indexes = self[delta_report_key][path] = dict_()
+ iterable_items_added_at_indexes[param] = item
+
+ def _from_tree_type_changes(self, tree):
+ if 'type_changes' in tree:
+ for change in tree['type_changes']:
+ include_values = None
+ if type(change.t1) is type:
+ include_values = False
+ old_type = change.t1
+ new_type = change.t2
+ else:
+ old_type = get_type(change.t1)
+ new_type = get_type(change.t2)
+ include_values = True
+ try:
+ if new_type in numpy_numbers:
+ new_t1 = change.t1.astype(new_type)
+ include_values = not np.array_equal(new_t1, change.t2)
+ else:
+ new_t1 = new_type(change.t1)
+ # If simply applying the type from one value converts it to the other value,
+ # there is no need to include the actual values in the delta.
+ include_values = new_t1 != change.t2
+ except Exception:
+ pass
+
+ path = change.path(force=FORCE_DEFAULT)
+ new_path = change.path(use_t2=True, force=FORCE_DEFAULT)
+ remap_dict = RemapDict({
+ 'old_type': old_type,
+ 'new_type': new_type,
+ })
+ if path != new_path:
+ remap_dict['new_path'] = new_path
+ self['type_changes'][path] = remap_dict
+ if include_values or self.always_include_values:
+ remap_dict.update(old_value=change.t1, new_value=change.t2)
+
+ def _from_tree_value_changed(self, tree):
+ if 'values_changed' in tree:
+ for change in tree['values_changed']:
+ path = change.path(force=FORCE_DEFAULT)
+ new_path = change.path(use_t2=True, force=FORCE_DEFAULT)
+ the_changed = {'new_value': change.t2, 'old_value': change.t1}
+ if path != new_path:
+ the_changed['new_path'] = new_path
+ self['values_changed'][path] = the_changed
+ # If we ever want to store the difflib results instead of the new_value
+ # these lines need to be uncommented and the Delta object needs to be able
+ # to use them.
+ # if 'diff' in change.additional:
+ # the_changed.update({'diff': change.additional['diff']})
+
+ def _from_tree_repetition_change(self, tree):
+ if 'repetition_change' in tree:
+ for change in tree['repetition_change']:
+ path, _, _ = change.path(get_parent_too=True)
+ repetition = RemapDict(change.additional['repetition'])
+ value = change.t1
+ try:
+ iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path]
+ except KeyError:
+ iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path] = dict_()
+ for index in repetition['new_indexes']:
+ iterable_items_added_at_indexes[index] = value
+
+ def _from_tree_iterable_item_moved(self, tree):
+ if 'iterable_item_moved' in tree:
+ for change in tree['iterable_item_moved']:
+ if (
+ change.up.path(force=FORCE_DEFAULT) not in self["_iterable_opcodes"]
+ ):
+ the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2}
+ self['iterable_item_moved'][change.path(
+ force=FORCE_DEFAULT)] = the_changed
+
+
+class DiffLevel:
+ """
+ An object of this class represents a single object-tree-level in a reported change.
+ A double-linked list of these object describes a single change on all of its levels.
+ Looking at the tree of all changes, a list of those objects represents a single path through the tree
+ (which is just fancy for "a change").
+ This is the result object class for object reference style reports.
+
+ Example:
+
+ >>> t1 = {2: 2, 4: 44}
+ >>> t2 = {2: "b", 5: 55}
+ >>> ddiff = DeepDiff(t1, t2, view='tree')
+ >>> ddiff
+ {'dictionary_item_added': {<DiffLevel id:4560126096, t1:None, t2:55>},
+ 'dictionary_item_removed': {<DiffLevel id:4560126416, t1:44, t2:None>},
+ 'type_changes': {<DiffLevel id:4560126608, t1:2, t2:b>}}
+
+ Graph:
+
+ <DiffLevel id:123, original t1,t2> <DiffLevel id:200, original t1,t2>
+ ↑up ↑up
+ | |
+ | ChildRelationship | ChildRelationship
+ | |
+ ↓down ↓down
+ <DiffLevel id:13, t1:None, t2:55> <DiffLevel id:421, t1:44, t2:None>
+ .path() = 'root[5]' .path() = 'root[4]'
+
+ Note that the 2 top level DiffLevel objects are 2 different objects even though
+ they are essentially talking about the same diff operation.
+
+
+ A ChildRelationship object describing the relationship between t1 and it's child object,
+ where t1's child object equals down.t1.
+
+ Think about it like a graph:
+
+ +---------------------------------------------------------------+
+ | |
+ | parent difflevel parent |
+ | + ^ + |
+ +------|--------------------------|---------------------|-------+
+ | | | up |
+ | Child | | | ChildRelationship
+ | Relationship | | |
+ | down | | |
+ +------|----------------------|-------------------------|-------+
+ | v v v |
+ | child difflevel child |
+ | |
+ +---------------------------------------------------------------+
+
+
+ The child_rel example:
+
+ # dictionary_item_removed is a set so in order to get an item from it:
+ >>> (difflevel,) = ddiff['dictionary_item_removed'])
+ >>> difflevel.up.t1_child_rel
+ <DictRelationship id:456, parent:{2: 2, 4: 44}, child:44, param:4>
+
+ >>> (difflevel,) = ddiff['dictionary_item_added'])
+ >>> difflevel
+ <DiffLevel id:4560126096, t1:None, t2:55>
+
+ >>> difflevel.up
+ >>> <DiffLevel id:4560154512, t1:{2: 2, 4: 44}, t2:{2: 'b', 5: 55}>
+
+ >>> difflevel.up
+ <DiffLevel id:4560154512, t1:{2: 2, 4: 44}, t2:{2: 'b', 5: 55}>
+
+ # t1 didn't exist
+ >>> difflevel.up.t1_child_rel
+
+ # t2 is added
+ >>> difflevel.up.t2_child_rel
+ <DictRelationship id:4560154384, parent:{2: 'b', 5: 55}, child:55, param:5>
+
+ """
+
+ def __init__(self,
+ t1,
+ t2,
+ down=None,
+ up=None,
+ report_type=None,
+ child_rel1=None,
+ child_rel2=None,
+ additional=None,
+ verbose_level=1):
+ """
+ :param child_rel1: Either:
+ - An existing ChildRelationship object describing the "down" relationship for t1; or
+ - A ChildRelationship subclass. In this case, we will create the ChildRelationship objects
+ for both t1 and t2.
+ Alternatives for child_rel1 and child_rel2 must be used consistently.
+ :param child_rel2: Either:
+ - An existing ChildRelationship object describing the "down" relationship for t2; or
+ - The param argument for a ChildRelationship class we shall create.
+ Alternatives for child_rel1 and child_rel2 must be used consistently.
+ """
+
+ # The current-level object in the left hand tree
+ self.t1 = t1
+
+ # The current-level object in the right hand tree
+ self.t2 = t2
+
+ # Another DiffLevel object describing this change one level deeper down the object tree
+ self.down = down
+
+ # Another DiffLevel object describing this change one level further up the object tree
+ self.up = up
+
+ self.report_type = report_type
+
+ # If this object is this change's deepest level, this contains a string describing the type of change.
+ # Examples: "set_item_added", "values_changed"
+
+ # Note: don't use {} as additional's default value - this would turn out to be always the same dict object
+ self.additional = dict_() if additional is None else additional
+
+ # For some types of changes we store some additional information.
+ # This is a dict containing this information.
+ # Currently, this is used for:
+ # - values_changed: In case the changes data is a multi-line string,
+ # we include a textual diff as additional['diff'].
+ # - repetition_change: additional['repetition']:
+ # e.g. {'old_repeat': 2, 'new_repeat': 1, 'old_indexes': [0, 2], 'new_indexes': [2]}
+ # the user supplied ChildRelationship objects for t1 and t2
+
+ # A ChildRelationship object describing the relationship between t1 and it's child object,
+ # where t1's child object equals down.t1.
+ # If this relationship is representable as a string, str(self.t1_child_rel) returns a formatted param parsable python string,
+ # e.g. "[2]", ".my_attribute"
+ self.t1_child_rel = child_rel1
+
+ # Another ChildRelationship object describing the relationship between t2 and it's child object.
+ self.t2_child_rel = child_rel2
+
+ # Will cache result of .path() per 'force' as key for performance
+ self._path = dict_()
+
+ self.verbose_level = verbose_level
+
+ def __repr__(self):
+ if self.verbose_level:
+ from deepdiff.summarize import summarize
+
+ if self.additional:
+ additional_repr = summarize(self.additional, max_length=35)
+ result = "<{} {}>".format(self.path(), additional_repr)
+ else:
+ t1_repr = summarize(self.t1, max_length=35)
+ t2_repr = summarize(self.t2, max_length=35)
+ result = "<{} t1:{}, t2:{}>".format(self.path(), t1_repr, t2_repr)
+ else:
+ result = "<{}>".format(self.path())
+ return result
+
+ def __setattr__(self, key, value):
+ # Setting up or down, will set the opposite link in this linked list.
+ if key in UP_DOWN and value is not None:
+ self.__dict__[key] = value
+ opposite_key = UP_DOWN[key]
+ value.__dict__[opposite_key] = self
+ else:
+ self.__dict__[key] = value
+
+ def __iter__(self):
+ yield self.t1
+ yield self.t2
+
+ @property
+ def repetition(self):
+ return self.additional['repetition']
+
+ def auto_generate_child_rel(self, klass, param, param2=None):
+ """
+ Auto-populate self.child_rel1 and self.child_rel2.
+ This requires self.down to be another valid DiffLevel object.
+ :param klass: A ChildRelationship subclass describing the kind of parent-child relationship,
+ e.g. DictRelationship.
+ :param param: A ChildRelationship subclass-dependent parameter describing how to get from parent to child,
+ e.g. the key in a dict
+ """
+ if self.down.t1 is not notpresent: # type: ignore
+ self.t1_child_rel = ChildRelationship.create(
+ klass=klass, parent=self.t1, child=self.down.t1, param=param) # type: ignore
+ if self.down.t2 is not notpresent: # type: ignore
+ self.t2_child_rel = ChildRelationship.create(
+ klass=klass, parent=self.t2, child=self.down.t2, param=param if param2 is None else param2) # type: ignore
+
+ @property
+ def all_up(self):
+ """
+ Get the root object of this comparison.
+ (This is a convenient wrapper for following the up attribute as often as you can.)
+ :rtype: DiffLevel
+ """
+ level = self
+ while level.up:
+ level = level.up
+ return level
+
+ @property
+ def all_down(self):
+ """
+ Get the leaf object of this comparison.
+ (This is a convenient wrapper for following the down attribute as often as you can.)
+ :rtype: DiffLevel
+ """
+ level = self
+ while level.down:
+ level = level.down
+ return level
+
+ @staticmethod
+ def _format_result(root, result):
+ return None if result is None else "{}{}".format(root, result)
+
+ def get_root_key(self, use_t2=False):
+ """
+ Get the path's root key value for this change
+
+ For example if the path to the element that is reported to have a change in value is root['X'][0]
+ then get_root_key should return 'X'
+ """
+ root_level = self.all_up
+ if(use_t2):
+ next_rel = root_level.t2_child_rel
+ else:
+ next_rel = root_level.t1_child_rel or root_level.t2_child_rel # next relationship object to get a formatted param from
+
+ if next_rel:
+ return next_rel.param
+ return notpresent
+
+ def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'):
+ """
+ A python syntax string describing how to descend to this level, assuming the top level object is called root.
+ Returns None if the path is not representable as a string.
+ This might be the case for example if there are sets involved (because then there's not path at all) or because
+ custom objects used as dictionary keys (then there is a path but it's not representable).
+ Example: root['ingredients'][0]
+ Note: We will follow the left side of the comparison branch, i.e. using the t1's to build the path.
+ Using t1 or t2 should make no difference at all, except for the last step of a child-added/removed relationship.
+ If it does in any other case, your comparison path is corrupt.
+
+ **Parameters**
+
+ :param root: The result string shall start with this var name
+ :param force: Bends the meaning of "no string representation".
+ If None:
+ Will strictly return Python-parsable expressions. The result those yield will compare
+ equal to the objects in question.
+ If 'yes':
+ Will return a path including '(unrepresentable)' in place of non string-representable parts.
+ If 'fake':
+ Will try to produce an output optimized for readability.
+ This will pretend all iterables are subscriptable, for example.
+ :param output_format: The format of the output. The options are 'str' which is the default and produces a
+ string representation of the path or 'list' to produce a list of keys and attributes
+ that produce the path.
+ """
+ # TODO: We could optimize this by building on top of self.up's path if it is cached there
+ cache_key = "{}{}{}{}".format(force, get_parent_too, use_t2, output_format)
+ if cache_key in self._path:
+ cached = self._path[cache_key]
+ if get_parent_too:
+ parent, param, result = cached
+ return (self._format_result(root, parent), param, self._format_result(root, result))
+ else:
+ return self._format_result(root, cached)
+
+ if output_format == 'str':
+ result = parent = param = ""
+ else:
+ result = []
+
+ level = self.all_up # start at the root
+
+ # traverse all levels of this relationship
+ while level and level is not self:
+ # get this level's relationship object
+ if use_t2:
+ next_rel = level.t2_child_rel or level.t1_child_rel
+ else:
+ next_rel = level.t1_child_rel or level.t2_child_rel # next relationship object to get a formatted param from
+
+ # t1 and t2 both are empty
+ if next_rel is None:
+ break
+
+ # Build path for this level
+ if output_format == 'str':
+ item = next_rel.get_param_repr(force)
+ if item:
+ parent = result
+ param = next_rel.param
+ result += item
+ else:
+ # it seems this path is not representable as a string
+ result = None
+ break
+ elif output_format == 'list':
+ result.append(next_rel.param) # type: ignore
+
+ # Prepare processing next level
+ level = level.down
+
+ if output_format == 'str':
+ if get_parent_too:
+ self._path[cache_key] = (parent, param, result) # type: ignore
+ output = (self._format_result(root, parent), param, self._format_result(root, result)) # type: ignore
+ else:
+ self._path[cache_key] = result
+ output = self._format_result(root, result)
+ else:
+ output = result
+ return output
+
+ def create_deeper(self,
+ new_t1,
+ new_t2,
+ child_relationship_class,
+ child_relationship_param=None,
+ child_relationship_param2=None,
+ report_type=None):
+ """
+ Start a new comparison level and correctly link it to this one.
+ :rtype: DiffLevel
+ :return: New level
+ """
+ level = self.all_down
+ result = DiffLevel(
+ new_t1, new_t2, down=None, up=level, report_type=report_type, verbose_level=self.verbose_level)
+ level.down = result
+ level.auto_generate_child_rel(
+ klass=child_relationship_class, param=child_relationship_param, param2=child_relationship_param2)
+ return result
+
+ def branch_deeper(self,
+ new_t1,
+ new_t2,
+ child_relationship_class,
+ child_relationship_param=None,
+ child_relationship_param2=None,
+ report_type=None):
+ """
+ Branch this comparison: Do not touch this comparison line, but create a new one with exactly the same content,
+ just one level deeper.
+ :rtype: DiffLevel
+ :return: New level in new comparison line
+ """
+ branch = self.copy()
+ return branch.create_deeper(new_t1, new_t2, child_relationship_class,
+ child_relationship_param, child_relationship_param2, report_type)
+
+ def copy(self):
+ """
+ Get a deep copy of this comparision line.
+ :return: The leaf ("downmost") object of the copy.
+ """
+ orig = self.all_up
+ result = copy(orig) # copy top level
+
+ while orig is not None:
+ result.additional = copy(orig.additional)
+
+ if orig.down is not None: # copy and create references to the following level
+ # copy following level
+ result.down = copy(orig.down)
+
+ if orig.t1_child_rel is not None:
+ result.t1_child_rel = ChildRelationship.create(
+ klass=orig.t1_child_rel.__class__,
+ parent=result.t1,
+ child=result.down.t1,
+ param=orig.t1_child_rel.param)
+ if orig.t2_child_rel is not None:
+ result.t2_child_rel = ChildRelationship.create(
+ klass=orig.t2_child_rel.__class__,
+ parent=result.t2,
+ child=result.down.t2,
+ param=orig.t2_child_rel.param)
+
+ # descend to next level
+ orig = orig.down
+ if result.down is not None:
+ result = result.down
+ return result
+
+
+class ChildRelationship:
+ """
+ Describes the relationship between a container object (the "parent") and the contained
+ "child" object.
+ """
+
+ # Format to a be used for representing param.
+ # E.g. for a dict, this turns a formatted param param "42" into "[42]".
+ param_repr_format = None
+
+ # This is a hook allowing subclasses to manipulate param strings.
+ # :param string: Input string
+ # :return: Manipulated string, as appropriate in this context.
+ quote_str = None
+
+ @staticmethod
+ def create(klass, parent, child, param=None):
+ if not issubclass(klass, ChildRelationship):
+ raise TypeError
+ return klass(parent, child, param)
+
+ def __init__(self, parent, child, param=None):
+ # The parent object of this relationship, e.g. a dict
+ self.parent = parent
+
+ # The child object of this relationship, e.g. a value in a dict
+ self.child = child
+
+ # A subclass-dependent parameter describing how to get from parent to child, e.g. the key in a dict
+ self.param = param
+
+ def __repr__(self):
+ from deepdiff.summarize import summarize
+
+ name = "<{} parent:{}, child:{}, param:{}>"
+ parent = summarize(self.parent, max_length=35)
+ child = summarize(self.child, max_length=35)
+ param = summarize(self.param, max_length=15)
+ return name.format(self.__class__.__name__, parent, child, param)
+
+ def get_param_repr(self, force=None):
+ """
+ Returns a formatted param python parsable string describing this relationship,
+ or None if the relationship is not representable as a string.
+ This string can be appended to the parent Name.
+ Subclasses representing a relationship that cannot be expressed as a string override this method to return None.
+ Examples: "[2]", ".attribute", "['mykey']"
+ :param force: Bends the meaning of "no string representation".
+ If None:
+ Will strictly return partials of Python-parsable expressions. The result those yield will compare
+ equal to the objects in question.
+ If 'yes':
+ Will return a formatted param including '(unrepresentable)' instead of the non string-representable part.
+
+ """
+ return self.stringify_param(force)
+
+ def stringify_param(self, force=None):
+ """
+ Convert param to a string. Return None if there is no string representation.
+ This is called by get_param_repr()
+ :param force: Bends the meaning of "no string representation".
+ If None:
+ Will strictly return Python-parsable expressions. The result those yield will compare
+ equal to the objects in question.
+ If 'yes':
+ Will return '(unrepresentable)' instead of None if there is no string representation
+
+ TODO: stringify_param has issues with params that when converted to string via repr,
+ it is not straight forward to turn them back into the original object.
+ Although repr is meant to be able to reconstruct the original object but for complex objects, repr
+ often does not recreate the original object.
+ Perhaps we should log that the repr reconstruction failed so the user is aware.
+ """
+ param = self.param
+ if isinstance(param, strings):
+ result = stringify_element(param, quote_str=self.quote_str)
+ elif isinstance(param, tuple): # Currently only for numpy ndarrays
+ result = ']['.join(map(repr, param))
+ elif hasattr(param, '__dataclass_fields__'):
+ attrs_to_values = [f"{key}={value}" for key, value in [(i, getattr(param, i)) for i in param.__dataclass_fields__]] # type: ignore
+ result = f"{param.__class__.__name__}({','.join(attrs_to_values)})"
+ else:
+ candidate = repr(param)
+ try:
+ resurrected = literal_eval_extended(candidate)
+ # Note: This will miss string-representable custom objects.
+ # However, the only alternative I can currently think of is using eval() which is inherently dangerous.
+ except (SyntaxError, ValueError) as err:
+ logger.error(
+ f'stringify_param was not able to get a proper repr for "{param}". '
+ "This object will be reported as None. Add instructions for this object to DeepDiff's "
+ f"helper.literal_eval_extended to make it work properly: {err}")
+ result = None
+ else:
+ result = candidate if resurrected == param else None
+
+ if result:
+ result = ':' if self.param_repr_format is None else self.param_repr_format.format(result)
+
+ return result
+
+
+class DictRelationship(ChildRelationship):
+ param_repr_format = "[{}]"
+ quote_str = "'{}'"
+
+
+class NumpyArrayRelationship(ChildRelationship):
+ param_repr_format = "[{}]"
+ quote_str = None
+
+
+class SubscriptableIterableRelationship(DictRelationship):
+ pass
+
+
+class InaccessibleRelationship(ChildRelationship):
+ pass
+
+
+# there is no random access to set elements
+class SetRelationship(InaccessibleRelationship):
+ pass
+
+
+class NonSubscriptableIterableRelationship(InaccessibleRelationship):
+
+ param_repr_format = "[{}]"
+
+ def get_param_repr(self, force=None):
+ if force == 'yes':
+ result = "(unrepresentable)"
+ elif force == 'fake' and self.param:
+ result = self.stringify_param()
+ else:
+ result = None
+
+ return result
+
+
+class AttributeRelationship(ChildRelationship):
+ param_repr_format = ".{}"