diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/deepdiff/model.py | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/deepdiff/model.py')
-rw-r--r-- | .venv/lib/python3.12/site-packages/deepdiff/model.py | 974 |
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 = ".{}" |