1import logging 2from collections.abc import Mapping 3from copy import copy 4from ordered_set import OrderedSet 5from deepdiff.helper import ( 6 RemapDict, strings, short_repr, notpresent, get_type, numpy_numbers, np, literal_eval_extended, 7 dict_) 8 9logger = logging.getLogger(__name__) 10 11FORCE_DEFAULT = 'fake' 12UP_DOWN = {'up': 'down', 'down': 'up'} 13 14REPORT_KEYS = { 15 "type_changes", 16 "dictionary_item_added", 17 "dictionary_item_removed", 18 "values_changed", 19 "unprocessed", 20 "iterable_item_added", 21 "iterable_item_removed", 22 "iterable_item_moved", 23 "attribute_added", 24 "attribute_removed", 25 "set_item_removed", 26 "set_item_added", 27 "repetition_change", 28} 29 30CUSTOM_FIELD = "__internal:custom:extra_info" 31 32 33class DoesNotExist(Exception): 34 pass 35 36 37class ResultDict(RemapDict): 38 39 def remove_empty_keys(self): 40 """ 41 Remove empty keys from this object. Should always be called after the result is final. 42 :return: 43 """ 44 empty_keys = [k for k, v in self.items() if not v] 45 46 for k in empty_keys: 47 del self[k] 48 49 50class PrettyOrderedSet(OrderedSet): 51 """ 52 From the perspective of the users of the library, they are dealing with lists. 53 Behind the scene, we have ordered sets. 54 """ 55 56 def __repr__(self): 57 return '[{}]'.format(", ".join(map(str, self))) 58 59 60class TreeResult(ResultDict): 61 def __init__(self): 62 for key in REPORT_KEYS: 63 self[key] = PrettyOrderedSet() 64 65 def mutual_add_removes_to_become_value_changes(self): 66 """ 67 There might be the same paths reported in the results as removed and added. 68 In such cases they should be reported as value_changes. 69 70 Note that this function mutates the tree in ways that causes issues when report_repetition=True 71 and should be avoided in that case. 72 73 This function should only be run on the Tree Result. 74 """ 75 if self.get('iterable_item_added') and self.get('iterable_item_removed'): 76 added_paths = {i.path(): i for i in self['iterable_item_added']} 77 removed_paths = {i.path(): i for i in self['iterable_item_removed']} 78 mutual_paths = set(added_paths) & set(removed_paths) 79 80 if mutual_paths and 'values_changed' not in self: 81 self['values_changed'] = PrettyOrderedSet() 82 for path in mutual_paths: 83 level_before = removed_paths[path] 84 self['iterable_item_removed'].remove(level_before) 85 level_after = added_paths[path] 86 self['iterable_item_added'].remove(level_after) 87 level_before.t2 = level_after.t2 88 self['values_changed'].add(level_before) 89 if 'iterable_item_removed' in self and not self['iterable_item_removed']: 90 del self['iterable_item_removed'] 91 if 'iterable_item_added' in self and not self['iterable_item_added']: 92 del self['iterable_item_added'] 93 94 def __getitem__(self, item): 95 if item not in self: 96 self[item] = PrettyOrderedSet() 97 return self.get(item) 98 99 100class TextResult(ResultDict): 101 ADD_QUOTES_TO_STRINGS = True 102 103 def __init__(self, tree_results=None, verbose_level=1): 104 self.verbose_level = verbose_level 105 # TODO: centralize keys 106 self.update({ 107 "type_changes": dict_(), 108 "dictionary_item_added": self.__set_or_dict(), 109 "dictionary_item_removed": self.__set_or_dict(), 110 "values_changed": dict_(), 111 "unprocessed": [], 112 "iterable_item_added": dict_(), 113 "iterable_item_removed": dict_(), 114 "iterable_item_moved": dict_(), 115 "attribute_added": self.__set_or_dict(), 116 "attribute_removed": self.__set_or_dict(), 117 "set_item_removed": PrettyOrderedSet(), 118 "set_item_added": PrettyOrderedSet(), 119 "repetition_change": dict_() 120 }) 121 122 if tree_results: 123 self._from_tree_results(tree_results) 124 125 def __set_or_dict(self): 126 return {} if self.verbose_level >= 2 else PrettyOrderedSet() 127 128 def _from_tree_results(self, tree): 129 """ 130 Populate this object by parsing an existing reference-style result dictionary. 131 :param tree: A TreeResult 132 :return: 133 """ 134 self._from_tree_type_changes(tree) 135 self._from_tree_default(tree, 'dictionary_item_added') 136 self._from_tree_default(tree, 'dictionary_item_removed') 137 self._from_tree_value_changed(tree) 138 self._from_tree_unprocessed(tree) 139 self._from_tree_default(tree, 'iterable_item_added') 140 self._from_tree_default(tree, 'iterable_item_removed') 141 self._from_tree_iterable_item_moved(tree) 142 self._from_tree_default(tree, 'attribute_added') 143 self._from_tree_default(tree, 'attribute_removed') 144 self._from_tree_set_item_removed(tree) 145 self._from_tree_set_item_added(tree) 146 self._from_tree_repetition_change(tree) 147 self._from_tree_deep_distance(tree) 148 self._from_tree_custom_results(tree) 149 150 def _from_tree_default(self, tree, report_type): 151 if report_type in tree: 152 for change in tree[report_type]: # report each change 153 # determine change direction (added or removed) 154 # Report t2 (the new one) whenever possible. 155 # In cases where t2 doesn't exist (i.e. stuff removed), report t1. 156 if change.t2 is not notpresent: 157 item = change.t2 158 else: 159 item = change.t1 160 161 # do the reporting 162 report = self[report_type] 163 if isinstance(report, PrettyOrderedSet): 164 report.add(change.path(force=FORCE_DEFAULT)) 165 elif isinstance(report, dict): 166 report[change.path(force=FORCE_DEFAULT)] = item 167 elif isinstance(report, list): # pragma: no cover 168 # we don't actually have any of those right now, but just in case 169 report.append(change.path(force=FORCE_DEFAULT)) 170 else: # pragma: no cover 171 # should never happen 172 raise TypeError("Cannot handle {} report container type.". 173 format(report)) 174 175 def _from_tree_type_changes(self, tree): 176 if 'type_changes' in tree: 177 for change in tree['type_changes']: 178 if type(change.t1) is type: 179 include_values = False 180 old_type = change.t1 181 new_type = change.t2 182 else: 183 include_values = True 184 old_type = get_type(change.t1) 185 new_type = get_type(change.t2) 186 remap_dict = RemapDict({ 187 'old_type': old_type, 188 'new_type': new_type 189 }) 190 self['type_changes'][change.path( 191 force=FORCE_DEFAULT)] = remap_dict 192 if self.verbose_level and include_values: 193 remap_dict.update(old_value=change.t1, new_value=change.t2) 194 195 def _from_tree_value_changed(self, tree): 196 if 'values_changed' in tree and self.verbose_level > 0: 197 for change in tree['values_changed']: 198 the_changed = {'new_value': change.t2, 'old_value': change.t1} 199 self['values_changed'][change.path( 200 force=FORCE_DEFAULT)] = the_changed 201 if 'diff' in change.additional: 202 the_changed.update({'diff': change.additional['diff']}) 203 204 def _from_tree_iterable_item_moved(self, tree): 205 if 'iterable_item_moved' in tree and self.verbose_level > 1: 206 for change in tree['iterable_item_moved']: 207 the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2} 208 self['iterable_item_moved'][change.path( 209 force=FORCE_DEFAULT)] = the_changed 210 211 def _from_tree_unprocessed(self, tree): 212 if 'unprocessed' in tree: 213 for change in tree['unprocessed']: 214 self['unprocessed'].append("{}: {} and {}".format(change.path( 215 force=FORCE_DEFAULT), change.t1, change.t2)) 216 217 def _from_tree_set_item_added_or_removed(self, tree, key): 218 if key in tree: 219 set_item_info = self[key] 220 is_dict = isinstance(set_item_info, Mapping) 221 for change in tree[key]: 222 path = change.up.path( 223 ) # we want't the set's path, the added item is not directly accessible 224 item = change.t2 if key == 'set_item_added' else change.t1 225 if self.ADD_QUOTES_TO_STRINGS and isinstance(item, strings): 226 item = "'%s'" % item 227 if is_dict: 228 if path not in set_item_info: 229 set_item_info[path] = set() 230 set_item_info[path].add(item) 231 else: 232 set_item_info.add("{}[{}]".format(path, str(item))) 233 # this syntax is rather peculiar, but it's DeepDiff 2.x compatible) 234 235 def _from_tree_set_item_added(self, tree): 236 self._from_tree_set_item_added_or_removed(tree, key='set_item_added') 237 238 def _from_tree_set_item_removed(self, tree): 239 self._from_tree_set_item_added_or_removed(tree, key='set_item_removed') 240 241 def _from_tree_repetition_change(self, tree): 242 if 'repetition_change' in tree: 243 for change in tree['repetition_change']: 244 path = change.path(force=FORCE_DEFAULT) 245 self['repetition_change'][path] = RemapDict( 246 change.additional['repetition'] 247 ) 248 self['repetition_change'][path]['value'] = change.t1 249 250 def _from_tree_deep_distance(self, tree): 251 if 'deep_distance' in tree: 252 self['deep_distance'] = tree['deep_distance'] 253 254 def _from_tree_custom_results(self, tree): 255 for k, _level_list in tree.items(): 256 if k not in REPORT_KEYS: 257 if not isinstance(_level_list, PrettyOrderedSet): 258 continue 259 260 # if len(_level_list) == 0: 261 # continue 262 # 263 # if not isinstance(_level_list[0], DiffLevel): 264 # continue 265 266 # _level_list is a list of DiffLevel 267 _custom_dict = {} 268 for _level in _level_list: 269 _custom_dict[_level.path( 270 force=FORCE_DEFAULT)] = _level.additional.get(CUSTOM_FIELD, {}) 271 self[k] = _custom_dict 272 273 274class DeltaResult(TextResult): 275 ADD_QUOTES_TO_STRINGS = False 276 277 def __init__(self, tree_results=None, ignore_order=None): 278 self.ignore_order = ignore_order 279 280 self.update({ 281 "type_changes": dict_(), 282 "dictionary_item_added": dict_(), 283 "dictionary_item_removed": dict_(), 284 "values_changed": dict_(), 285 "iterable_item_added": dict_(), 286 "iterable_item_removed": dict_(), 287 "iterable_item_moved": dict_(), 288 "attribute_added": dict_(), 289 "attribute_removed": dict_(), 290 "set_item_removed": dict_(), 291 "set_item_added": dict_(), 292 "iterable_items_added_at_indexes": dict_(), 293 "iterable_items_removed_at_indexes": dict_(), 294 }) 295 296 if tree_results: 297 self._from_tree_results(tree_results) 298 299 def _from_tree_results(self, tree): 300 """ 301 Populate this object by parsing an existing reference-style result dictionary. 302 :param tree: A TreeResult 303 :return: 304 """ 305 self._from_tree_type_changes(tree) 306 self._from_tree_default(tree, 'dictionary_item_added') 307 self._from_tree_default(tree, 'dictionary_item_removed') 308 self._from_tree_value_changed(tree) 309 if self.ignore_order: 310 self._from_tree_iterable_item_added_or_removed( 311 tree, 'iterable_item_added', delta_report_key='iterable_items_added_at_indexes') 312 self._from_tree_iterable_item_added_or_removed( 313 tree, 'iterable_item_removed', delta_report_key='iterable_items_removed_at_indexes') 314 else: 315 self._from_tree_default(tree, 'iterable_item_added') 316 self._from_tree_default(tree, 'iterable_item_removed') 317 self._from_tree_iterable_item_moved(tree) 318 self._from_tree_default(tree, 'attribute_added') 319 self._from_tree_default(tree, 'attribute_removed') 320 self._from_tree_set_item_removed(tree) 321 self._from_tree_set_item_added(tree) 322 self._from_tree_repetition_change(tree) 323 324 def _from_tree_iterable_item_added_or_removed(self, tree, report_type, delta_report_key): 325 if report_type in tree: 326 for change in tree[report_type]: # report each change 327 # determine change direction (added or removed) 328 # Report t2 (the new one) whenever possible. 329 # In cases where t2 doesn't exist (i.e. stuff removed), report t1. 330 if change.t2 is not notpresent: 331 item = change.t2 332 else: 333 item = change.t1 334 335 # do the reporting 336 path, param, _ = change.path(force=FORCE_DEFAULT, get_parent_too=True) 337 try: 338 iterable_items_added_at_indexes = self[delta_report_key][path] 339 except KeyError: 340 iterable_items_added_at_indexes = self[delta_report_key][path] = dict_() 341 iterable_items_added_at_indexes[param] = item 342 343 def _from_tree_type_changes(self, tree): 344 if 'type_changes' in tree: 345 for change in tree['type_changes']: 346 include_values = None 347 if type(change.t1) is type: 348 include_values = False 349 old_type = change.t1 350 new_type = change.t2 351 else: 352 old_type = get_type(change.t1) 353 new_type = get_type(change.t2) 354 include_values = True 355 try: 356 if new_type in numpy_numbers: 357 new_t1 = change.t1.astype(new_type) 358 include_values = not np.array_equal(new_t1, change.t2) 359 else: 360 new_t1 = new_type(change.t1) 361 # If simply applying the type from one value converts it to the other value, 362 # there is no need to include the actual values in the delta. 363 include_values = new_t1 != change.t2 364 except Exception: 365 pass 366 367 remap_dict = RemapDict({ 368 'old_type': old_type, 369 'new_type': new_type 370 }) 371 self['type_changes'][change.path( 372 force=FORCE_DEFAULT)] = remap_dict 373 if include_values: 374 remap_dict.update(old_value=change.t1, new_value=change.t2) 375 376 def _from_tree_value_changed(self, tree): 377 if 'values_changed' in tree: 378 for change in tree['values_changed']: 379 the_changed = {'new_value': change.t2, 'old_value': change.t1} 380 self['values_changed'][change.path( 381 force=FORCE_DEFAULT)] = the_changed 382 # If we ever want to store the difflib results instead of the new_value 383 # these lines need to be uncommented and the Delta object needs to be able 384 # to use them. 385 # if 'diff' in change.additional: 386 # the_changed.update({'diff': change.additional['diff']}) 387 388 def _from_tree_repetition_change(self, tree): 389 if 'repetition_change' in tree: 390 for change in tree['repetition_change']: 391 path, _, _ = change.path(get_parent_too=True) 392 repetition = RemapDict(change.additional['repetition']) 393 value = change.t1 394 try: 395 iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path] 396 except KeyError: 397 iterable_items_added_at_indexes = self['iterable_items_added_at_indexes'][path] = dict_() 398 for index in repetition['new_indexes']: 399 iterable_items_added_at_indexes[index] = value 400 401 def _from_tree_iterable_item_moved(self, tree): 402 if 'iterable_item_moved' in tree: 403 for change in tree['iterable_item_moved']: 404 the_changed = {'new_path': change.path(use_t2=True), 'value': change.t2} 405 self['iterable_item_moved'][change.path( 406 force=FORCE_DEFAULT)] = the_changed 407 408 409class DiffLevel: 410 """ 411 An object of this class represents a single object-tree-level in a reported change. 412 A double-linked list of these object describes a single change on all of its levels. 413 Looking at the tree of all changes, a list of those objects represents a single path through the tree 414 (which is just fancy for "a change"). 415 This is the result object class for object reference style reports. 416 417 Example: 418 419 >>> t1 = {2: 2, 4: 44} 420 >>> t2 = {2: "b", 5: 55} 421 >>> ddiff = DeepDiff(t1, t2, view='tree') 422 >>> ddiff 423 {'dictionary_item_added': {<DiffLevel id:4560126096, t1:None, t2:55>}, 424 'dictionary_item_removed': {<DiffLevel id:4560126416, t1:44, t2:None>}, 425 'type_changes': {<DiffLevel id:4560126608, t1:2, t2:b>}} 426 427 Graph: 428 429 <DiffLevel id:123, original t1,t2> <DiffLevel id:200, original t1,t2> 430 ↑up ↑up 431 | | 432 | ChildRelationship | ChildRelationship 433 | | 434 ↓down ↓down 435 <DiffLevel id:13, t1:None, t2:55> <DiffLevel id:421, t1:44, t2:None> 436 .path() = 'root[5]' .path() = 'root[4]' 437 438 Note that the 2 top level DiffLevel objects are 2 different objects even though 439 they are essentially talking about the same diff operation. 440 441 442 A ChildRelationship object describing the relationship between t1 and it's child object, 443 where t1's child object equals down.t1. 444 445 Think about it like a graph: 446 447 +---------------------------------------------------------------+ 448 | | 449 | parent difflevel parent | 450 | + ^ + | 451 +------|--------------------------|---------------------|-------+ 452 | | | up | 453 | Child | | | ChildRelationship 454 | Relationship | | | 455 | down | | | 456 +------|----------------------|-------------------------|-------+ 457 | v v v | 458 | child difflevel child | 459 | | 460 +---------------------------------------------------------------+ 461 462 463 The child_rel example: 464 465 # dictionary_item_removed is a set so in order to get an item from it: 466 >>> (difflevel,) = ddiff['dictionary_item_removed']) 467 >>> difflevel.up.t1_child_rel 468 <DictRelationship id:456, parent:{2: 2, 4: 44}, child:44, param:4> 469 470 >>> (difflevel,) = ddiff['dictionary_item_added']) 471 >>> difflevel 472 <DiffLevel id:4560126096, t1:None, t2:55> 473 474 >>> difflevel.up 475 >>> <DiffLevel id:4560154512, t1:{2: 2, 4: 44}, t2:{2: 'b', 5: 55}> 476 477 >>> difflevel.up 478 <DiffLevel id:4560154512, t1:{2: 2, 4: 44}, t2:{2: 'b', 5: 55}> 479 480 # t1 didn't exist 481 >>> difflevel.up.t1_child_rel 482 483 # t2 is added 484 >>> difflevel.up.t2_child_rel 485 <DictRelationship id:4560154384, parent:{2: 'b', 5: 55}, child:55, param:5> 486 487 """ 488 489 def __init__(self, 490 t1, 491 t2, 492 down=None, 493 up=None, 494 report_type=None, 495 child_rel1=None, 496 child_rel2=None, 497 additional=None, 498 verbose_level=1): 499 """ 500 :param child_rel1: Either: 501 - An existing ChildRelationship object describing the "down" relationship for t1; or 502 - A ChildRelationship subclass. In this case, we will create the ChildRelationship objects 503 for both t1 and t2. 504 Alternatives for child_rel1 and child_rel2 must be used consistently. 505 :param child_rel2: Either: 506 - An existing ChildRelationship object describing the "down" relationship for t2; or 507 - The param argument for a ChildRelationship class we shall create. 508 Alternatives for child_rel1 and child_rel2 must be used consistently. 509 """ 510 511 # The current-level object in the left hand tree 512 self.t1 = t1 513 514 # The current-level object in the right hand tree 515 self.t2 = t2 516 517 # Another DiffLevel object describing this change one level deeper down the object tree 518 self.down = down 519 520 # Another DiffLevel object describing this change one level further up the object tree 521 self.up = up 522 523 self.report_type = report_type 524 525 # If this object is this change's deepest level, this contains a string describing the type of change. 526 # Examples: "set_item_added", "values_changed" 527 528 # Note: don't use {} as additional's default value - this would turn out to be always the same dict object 529 self.additional = dict_() if additional is None else additional 530 531 # For some types of changes we store some additional information. 532 # This is a dict containing this information. 533 # Currently, this is used for: 534 # - values_changed: In case the changes data is a multi-line string, 535 # we include a textual diff as additional['diff']. 536 # - repetition_change: additional['repetition']: 537 # e.g. {'old_repeat': 2, 'new_repeat': 1, 'old_indexes': [0, 2], 'new_indexes': [2]} 538 # the user supplied ChildRelationship objects for t1 and t2 539 540 # A ChildRelationship object describing the relationship between t1 and it's child object, 541 # where t1's child object equals down.t1. 542 # If this relationship is representable as a string, str(self.t1_child_rel) returns a formatted param parsable python string, 543 # e.g. "[2]", ".my_attribute" 544 self.t1_child_rel = child_rel1 545 546 # Another ChildRelationship object describing the relationship between t2 and it's child object. 547 self.t2_child_rel = child_rel2 548 549 # Will cache result of .path() per 'force' as key for performance 550 self._path = dict_() 551 552 self.verbose_level = verbose_level 553 554 def __repr__(self): 555 if self.verbose_level: 556 if self.additional: 557 additional_repr = short_repr(self.additional, max_length=35) 558 result = "<{} {}>".format(self.path(), additional_repr) 559 else: 560 t1_repr = short_repr(self.t1) 561 t2_repr = short_repr(self.t2) 562 result = "<{} t1:{}, t2:{}>".format(self.path(), t1_repr, t2_repr) 563 else: 564 result = "<{}>".format(self.path()) 565 return result 566 567 def __setattr__(self, key, value): 568 # Setting up or down, will set the opposite link in this linked list. 569 if key in UP_DOWN and value is not None: 570 self.__dict__[key] = value 571 opposite_key = UP_DOWN[key] 572 value.__dict__[opposite_key] = self 573 else: 574 self.__dict__[key] = value 575 576 @property 577 def repetition(self): 578 return self.additional['repetition'] 579 580 def auto_generate_child_rel(self, klass, param, param2=None): 581 """ 582 Auto-populate self.child_rel1 and self.child_rel2. 583 This requires self.down to be another valid DiffLevel object. 584 :param klass: A ChildRelationship subclass describing the kind of parent-child relationship, 585 e.g. DictRelationship. 586 :param param: A ChildRelationship subclass-dependent parameter describing how to get from parent to child, 587 e.g. the key in a dict 588 """ 589 if self.down.t1 is not notpresent: 590 self.t1_child_rel = ChildRelationship.create( 591 klass=klass, parent=self.t1, child=self.down.t1, param=param) 592 if self.down.t2 is not notpresent: 593 self.t2_child_rel = ChildRelationship.create( 594 klass=klass, parent=self.t2, child=self.down.t2, param=param if param2 is None else param2) 595 596 @property 597 def all_up(self): 598 """ 599 Get the root object of this comparison. 600 (This is a convenient wrapper for following the up attribute as often as you can.) 601 :rtype: DiffLevel 602 """ 603 level = self 604 while level.up: 605 level = level.up 606 return level 607 608 @property 609 def all_down(self): 610 """ 611 Get the leaf object of this comparison. 612 (This is a convenient wrapper for following the down attribute as often as you can.) 613 :rtype: DiffLevel 614 """ 615 level = self 616 while level.down: 617 level = level.down 618 return level 619 620 @staticmethod 621 def _format_result(root, result): 622 return None if result is None else "{}{}".format(root, result) 623 624 def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'): 625 """ 626 A python syntax string describing how to descend to this level, assuming the top level object is called root. 627 Returns None if the path is not representable as a string. 628 This might be the case for example if there are sets involved (because then there's not path at all) or because 629 custom objects used as dictionary keys (then there is a path but it's not representable). 630 Example: root['ingredients'][0] 631 Note: We will follow the left side of the comparison branch, i.e. using the t1's to build the path. 632 Using t1 or t2 should make no difference at all, except for the last step of a child-added/removed relationship. 633 If it does in any other case, your comparison path is corrupt. 634 635 **Parameters** 636 637 :param root: The result string shall start with this var name 638 :param force: Bends the meaning of "no string representation". 639 If None: 640 Will strictly return Python-parsable expressions. The result those yield will compare 641 equal to the objects in question. 642 If 'yes': 643 Will return a path including '(unrepresentable)' in place of non string-representable parts. 644 If 'fake': 645 Will try to produce an output optimized for readability. 646 This will pretend all iterables are subscriptable, for example. 647 :param output_format: The format of the output. The options are 'str' which is the default and produces a 648 string representation of the path or 'list' to produce a list of keys and attributes 649 that produce the path. 650 """ 651 # TODO: We could optimize this by building on top of self.up's path if it is cached there 652 cache_key = "{}{}{}{}".format(force, get_parent_too, use_t2, output_format) 653 if cache_key in self._path: 654 cached = self._path[cache_key] 655 if get_parent_too: 656 parent, param, result = cached 657 return (self._format_result(root, parent), param, self._format_result(root, result)) 658 else: 659 return self._format_result(root, cached) 660 661 if output_format == 'str': 662 result = parent = param = "" 663 else: 664 result = [] 665 666 level = self.all_up # start at the root 667 668 # traverse all levels of this relationship 669 while level and level is not self: 670 # get this level's relationship object 671 if(use_t2): 672 next_rel = level.t2_child_rel 673 else: 674 next_rel = level.t1_child_rel or level.t2_child_rel # next relationship object to get a formatted param from 675 676 # t1 and t2 both are empty 677 if next_rel is None: 678 break 679 680 # Build path for this level 681 if output_format == 'str': 682 item = next_rel.get_param_repr(force) 683 if item: 684 parent = result 685 param = next_rel.param 686 result += item 687 else: 688 # it seems this path is not representable as a string 689 result = None 690 break 691 elif output_format == 'list': 692 result.append(next_rel.param) 693 694 # Prepare processing next level 695 level = level.down 696 697 if output_format == 'str': 698 if get_parent_too: 699 self._path[cache_key] = (parent, param, result) 700 output = (self._format_result(root, parent), param, self._format_result(root, result)) 701 else: 702 self._path[cache_key] = result 703 output = self._format_result(root, result) 704 else: 705 output = result 706 return output 707 708 def create_deeper(self, 709 new_t1, 710 new_t2, 711 child_relationship_class, 712 child_relationship_param=None, 713 child_relationship_param2=None, 714 report_type=None): 715 """ 716 Start a new comparison level and correctly link it to this one. 717 :rtype: DiffLevel 718 :return: New level 719 """ 720 level = self.all_down 721 result = DiffLevel( 722 new_t1, new_t2, down=None, up=level, report_type=report_type) 723 level.down = result 724 level.auto_generate_child_rel( 725 klass=child_relationship_class, param=child_relationship_param, param2=child_relationship_param2) 726 return result 727 728 def branch_deeper(self, 729 new_t1, 730 new_t2, 731 child_relationship_class, 732 child_relationship_param=None, 733 child_relationship_param2=None, 734 report_type=None): 735 """ 736 Branch this comparison: Do not touch this comparison line, but create a new one with exactly the same content, 737 just one level deeper. 738 :rtype: DiffLevel 739 :return: New level in new comparison line 740 """ 741 branch = self.copy() 742 return branch.create_deeper(new_t1, new_t2, child_relationship_class, 743 child_relationship_param, child_relationship_param2, report_type) 744 745 def copy(self): 746 """ 747 Get a deep copy of this comparision line. 748 :return: The leaf ("downmost") object of the copy. 749 """ 750 orig = self.all_up 751 result = copy(orig) # copy top level 752 753 while orig is not None: 754 result.additional = copy(orig.additional) 755 756 if orig.down is not None: # copy and create references to the following level 757 # copy following level 758 result.down = copy(orig.down) 759 760 if orig.t1_child_rel is not None: 761 result.t1_child_rel = ChildRelationship.create( 762 klass=orig.t1_child_rel.__class__, 763 parent=result.t1, 764 child=result.down.t1, 765 param=orig.t1_child_rel.param) 766 if orig.t2_child_rel is not None: 767 result.t2_child_rel = ChildRelationship.create( 768 klass=orig.t2_child_rel.__class__, 769 parent=result.t2, 770 child=result.down.t2, 771 param=orig.t2_child_rel.param) 772 773 # descend to next level 774 orig = orig.down 775 if result.down is not None: 776 result = result.down 777 return result 778 779 780class ChildRelationship: 781 """ 782 Describes the relationship between a container object (the "parent") and the contained 783 "child" object. 784 """ 785 786 # Format to a be used for representing param. 787 # E.g. for a dict, this turns a formatted param param "42" into "[42]". 788 param_repr_format = None 789 790 # This is a hook allowing subclasses to manipulate param strings. 791 # :param string: Input string 792 # :return: Manipulated string, as appropriate in this context. 793 quote_str = None 794 795 @staticmethod 796 def create(klass, parent, child, param=None): 797 if not issubclass(klass, ChildRelationship): 798 raise TypeError 799 return klass(parent, child, param) 800 801 def __init__(self, parent, child, param=None): 802 # The parent object of this relationship, e.g. a dict 803 self.parent = parent 804 805 # The child object of this relationship, e.g. a value in a dict 806 self.child = child 807 808 # A subclass-dependent parameter describing how to get from parent to child, e.g. the key in a dict 809 self.param = param 810 811 def __repr__(self): 812 name = "<{} parent:{}, child:{}, param:{}>" 813 parent = short_repr(self.parent) 814 child = short_repr(self.child) 815 param = short_repr(self.param) 816 return name.format(self.__class__.__name__, parent, child, param) 817 818 def get_param_repr(self, force=None): 819 """ 820 Returns a formatted param python parsable string describing this relationship, 821 or None if the relationship is not representable as a string. 822 This string can be appended to the parent Name. 823 Subclasses representing a relationship that cannot be expressed as a string override this method to return None. 824 Examples: "[2]", ".attribute", "['mykey']" 825 :param force: Bends the meaning of "no string representation". 826 If None: 827 Will strictly return partials of Python-parsable expressions. The result those yield will compare 828 equal to the objects in question. 829 If 'yes': 830 Will return a formatted param including '(unrepresentable)' instead of the non string-representable part. 831 832 """ 833 return self.stringify_param(force) 834 835 def stringify_param(self, force=None): 836 """ 837 Convert param to a string. Return None if there is no string representation. 838 This is called by get_param_repr() 839 :param force: Bends the meaning of "no string representation". 840 If None: 841 Will strictly return Python-parsable expressions. The result those yield will compare 842 equal to the objects in question. 843 If 'yes': 844 Will return '(unrepresentable)' instead of None if there is no string representation 845 846 TODO: stringify_param has issues with params that when converted to string via repr, 847 it is not straight forward to turn them back into the original object. 848 Although repr is meant to be able to reconstruct the original object but for complex objects, repr 849 often does not recreate the original object. 850 Perhaps we should log that the repr reconstruction failed so the user is aware. 851 """ 852 param = self.param 853 if isinstance(param, strings): 854 result = param if self.quote_str is None else self.quote_str.format(param) 855 elif isinstance(param, tuple): # Currently only for numpy ndarrays 856 result = ']['.join(map(repr, param)) 857 else: 858 candidate = repr(param) 859 try: 860 resurrected = literal_eval_extended(candidate) 861 # Note: This will miss string-representable custom objects. 862 # However, the only alternative I can currently think of is using eval() which is inherently dangerous. 863 except (SyntaxError, ValueError) as err: 864 logger.error( 865 f'stringify_param was not able to get a proper repr for "{param}". ' 866 "This object will be reported as None. Add instructions for this object to DeepDiff's " 867 f"helper.literal_eval_extended to make it work properly: {err}") 868 result = None 869 else: 870 result = candidate if resurrected == param else None 871 872 if result: 873 result = ':' if self.param_repr_format is None else self.param_repr_format.format(result) 874 875 return result 876 877 878class DictRelationship(ChildRelationship): 879 param_repr_format = "[{}]" 880 quote_str = "'{}'" 881 882 883class NumpyArrayRelationship(ChildRelationship): 884 param_repr_format = "[{}]" 885 quote_str = None 886 887 888class SubscriptableIterableRelationship(DictRelationship): 889 pass 890 891 892class InaccessibleRelationship(ChildRelationship): 893 pass 894 895 896# there is no random access to set elements 897class SetRelationship(InaccessibleRelationship): 898 pass 899 900 901class NonSubscriptableIterableRelationship(InaccessibleRelationship): 902 903 param_repr_format = "[{}]" 904 905 def get_param_repr(self, force=None): 906 if force == 'yes': 907 result = "(unrepresentable)" 908 elif force == 'fake' and self.param: 909 result = self.stringify_param() 910 else: 911 result = None 912 913 return result 914 915 916class AttributeRelationship(ChildRelationship): 917 param_repr_format = ".{}" 918