about summary refs log tree commit diff
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-master.tar.gz
two version of R2R are here HEAD master
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 = ".{}"