aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/deepdiff/model.py
blob: 41dd751750f47e7965d674f1f1be4141816dff9e (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
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 = ".{}"