23
23
VERIFICATION_MSG = 'Expected the old value for {} to be {} but it is {}. Error found on: {}'
24
24
ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.'
25
25
TYPE_CHANGE_FAIL_MSG = 'Unable to do the type change for {} from to type {} due to {}'
26
- VERIFY_SYMMETRY_MSG = ('while checking the symmetry of the delta. You have applied the delta to an object that has '
27
- 'different values than the original object the delta was made from' )
26
+ VERIFY_BIDIRECTIONAL_MSG = ('You have applied the delta to an object that has '
27
+ 'different values than the original object the delta was made from. ' )
28
28
FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}'
29
29
DELTA_NUMPY_OPERATOR_OVERRIDE_MSG = (
30
30
'A numpy ndarray is most likely being added to a delta. '
@@ -78,7 +78,9 @@ def __init__(
78
78
raise_errors = False ,
79
79
safe_to_import = None ,
80
80
serializer = pickle_dump ,
81
- verify_symmetry = False ,
81
+ verify_symmetry = None ,
82
+ bidirectional = False ,
83
+ always_include_values = False ,
82
84
force = False ,
83
85
):
84
86
if hasattr (deserializer , '__code__' ) and 'safe_to_import' in set (deserializer .__code__ .co_varnames ):
@@ -89,9 +91,21 @@ def _deserializer(obj, safe_to_import=None):
89
91
90
92
self ._reversed_diff = None
91
93
94
+ if verify_symmetry is not None :
95
+ logger .warning (
96
+ "DeepDiff Deprecation: use bidirectional instead of verify_symmetry parameter."
97
+ )
98
+ bidirectional = verify_symmetry
99
+
100
+ self .bidirectional = bidirectional
101
+ if bidirectional :
102
+ self .always_include_values = True # We need to include the values in bidirectional deltas
103
+ else :
104
+ self .always_include_values = always_include_values
105
+
92
106
if diff is not None :
93
107
if isinstance (diff , DeepDiff ):
94
- self .diff = diff ._to_delta_dict (directed = not verify_symmetry )
108
+ self .diff = diff ._to_delta_dict (directed = not bidirectional , always_include_values = self . always_include_values )
95
109
elif isinstance (diff , Mapping ):
96
110
self .diff = diff
97
111
elif isinstance (diff , strings ):
@@ -112,7 +126,6 @@ def _deserializer(obj, safe_to_import=None):
112
126
raise ValueError (DELTA_AT_LEAST_ONE_ARG_NEEDED )
113
127
114
128
self .mutate = mutate
115
- self .verify_symmetry = verify_symmetry
116
129
self .raise_errors = raise_errors
117
130
self .log_errors = log_errors
118
131
self ._numpy_paths = self .diff .pop ('_numpy_paths' , False )
@@ -162,16 +175,28 @@ def __add__(self, other):
162
175
163
176
__radd__ = __add__
164
177
178
+ def __rsub__ (self , other ):
179
+ if self ._reversed_diff is None :
180
+ self ._reversed_diff = self ._get_reverse_diff ()
181
+ self .diff , self ._reversed_diff = self ._reversed_diff , self .diff
182
+ result = self .__add__ (other )
183
+ self .diff , self ._reversed_diff = self ._reversed_diff , self .diff
184
+ return result
185
+
165
186
def _raise_or_log (self , msg , level = 'error' ):
166
187
if self .log_errors :
167
188
getattr (logger , level )(msg )
168
189
if self .raise_errors :
169
190
raise DeltaError (msg )
170
191
171
192
def _do_verify_changes (self , path , expected_old_value , current_old_value ):
172
- if self .verify_symmetry and expected_old_value != current_old_value :
193
+ if self .bidirectional and expected_old_value != current_old_value :
194
+ if isinstance (path , str ):
195
+ path_str = path
196
+ else :
197
+ path_str = stringify_path (path , root_element = ('' , GETATTR ))
173
198
self ._raise_or_log (VERIFICATION_MSG .format (
174
- path , expected_old_value , current_old_value , VERIFY_SYMMETRY_MSG ))
199
+ path_str , expected_old_value , current_old_value , VERIFY_BIDIRECTIONAL_MSG ))
175
200
176
201
def _get_elem_and_compare_to_old_value (self , obj , path_for_err_reporting , expected_old_value , elem = None , action = None , forced_old_value = None ):
177
202
try :
@@ -192,7 +217,7 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
192
217
current_old_value = not_found
193
218
if isinstance (path_for_err_reporting , (list , tuple )):
194
219
path_for_err_reporting = '.' .join ([i [0 ] for i in path_for_err_reporting ])
195
- if self .verify_symmetry :
220
+ if self .bidirectional :
196
221
self ._raise_or_log (VERIFICATION_MSG .format (
197
222
path_for_err_reporting ,
198
223
expected_old_value , current_old_value , e ))
@@ -357,7 +382,9 @@ def _do_type_changes(self):
357
382
358
383
def _do_post_process (self ):
359
384
if self .post_process_paths_to_convert :
360
- self ._do_values_or_type_changed (self .post_process_paths_to_convert , is_type_change = True )
385
+ # Example: We had converted some object to be mutable and now we are converting them back to be immutable.
386
+ # We don't need to check the change because it is not really a change that was part of the original diff.
387
+ self ._do_values_or_type_changed (self .post_process_paths_to_convert , is_type_change = True , verify_changes = False )
361
388
362
389
def _do_pre_process (self ):
363
390
if self ._numpy_paths and ('iterable_item_added' in self .diff or 'iterable_item_removed' in self .diff ):
@@ -394,7 +421,7 @@ def _get_elements_and_details(self, path):
394
421
return None
395
422
return elements , parent , parent_to_obj_elem , parent_to_obj_action , obj , elem , action
396
423
397
- def _do_values_or_type_changed (self , changes , is_type_change = False ):
424
+ def _do_values_or_type_changed (self , changes , is_type_change = False , verify_changes = True ):
398
425
for path , value in changes .items ():
399
426
elem_and_details = self ._get_elements_and_details (path )
400
427
if elem_and_details :
@@ -409,7 +436,7 @@ def _do_values_or_type_changed(self, changes, is_type_change=False):
409
436
continue # pragma: no cover. I have not been able to write a test for this case. But we should still check for it.
410
437
# With type change if we could have originally converted the type from old_value
411
438
# to new_value just by applying the class of the new_value, then we might not include the new_value
412
- # in the delta dictionary.
439
+ # in the delta dictionary. That is defined in Model.DeltaResult._from_tree_type_changes
413
440
if is_type_change and 'new_value' not in value :
414
441
try :
415
442
new_type = value ['new_type' ]
@@ -427,7 +454,8 @@ def _do_values_or_type_changed(self, changes, is_type_change=False):
427
454
self ._set_new_value (parent , parent_to_obj_elem , parent_to_obj_action ,
428
455
obj , elements , path , elem , action , new_value )
429
456
430
- self ._do_verify_changes (path , expected_old_value , current_old_value )
457
+ if verify_changes :
458
+ self ._do_verify_changes (path , expected_old_value , current_old_value )
431
459
432
460
def _do_item_removed (self , items ):
433
461
"""
@@ -580,8 +608,50 @@ def _do_ignore_order(self):
580
608
self ._simple_set_elem_value (obj = parent , path_for_err_reporting = path , elem = parent_to_obj_elem ,
581
609
value = new_obj , action = parent_to_obj_action )
582
610
583
- def _reverse_diff (self ):
584
- pass
611
+ def _get_reverse_diff (self ):
612
+ if not self .bidirectional :
613
+ raise ValueError ('Please recreate the delta with bidirectional=True' )
614
+
615
+ SIMPLE_ACTION_TO_REVERSE = {
616
+ 'iterable_item_added' : 'iterable_item_removed' ,
617
+ 'iterable_items_added_at_indexes' : 'iterable_items_removed_at_indexes' ,
618
+ 'attribute_added' : 'attribute_removed' ,
619
+ 'set_item_added' : 'set_item_removed' ,
620
+ 'dictionary_item_added' : 'dictionary_item_removed' ,
621
+ }
622
+ # Adding the reverse of the dictionary
623
+ for key in list (SIMPLE_ACTION_TO_REVERSE .keys ()):
624
+ SIMPLE_ACTION_TO_REVERSE [SIMPLE_ACTION_TO_REVERSE [key ]] = key
625
+
626
+ r_diff = {}
627
+ for action , info in self .diff .items ():
628
+ reverse_action = SIMPLE_ACTION_TO_REVERSE .get (action )
629
+ if reverse_action :
630
+ r_diff [reverse_action ] = info
631
+ elif action == 'values_changed' :
632
+ r_diff [action ] = {}
633
+ for path , path_info in info .items ():
634
+ r_diff [action ][path ] = {
635
+ 'new_value' : path_info ['old_value' ], 'old_value' : path_info ['new_value' ]
636
+ }
637
+ elif action == 'type_changes' :
638
+ r_diff [action ] = {}
639
+ for path , path_info in info .items ():
640
+ r_diff [action ][path ] = {
641
+ 'old_type' : path_info ['new_type' ], 'new_type' : path_info ['old_type' ],
642
+ }
643
+ if 'new_value' in path_info :
644
+ r_diff [action ][path ]['old_value' ] = path_info ['new_value' ]
645
+ if 'old_value' in path_info :
646
+ r_diff [action ][path ]['new_value' ] = path_info ['old_value' ]
647
+ elif action == 'iterable_item_moved' :
648
+ r_diff [action ] = {}
649
+ for path , path_info in info .items ():
650
+ old_path = path_info ['new_path' ]
651
+ r_diff [action ][old_path ] = {
652
+ 'new_path' : path , 'value' : path_info ['value' ],
653
+ }
654
+ return r_diff
585
655
586
656
def dump (self , file ):
587
657
"""
@@ -735,6 +805,7 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
735
805
Here are the list of actions that the flat dictionary can return.
736
806
iterable_item_added
737
807
iterable_item_removed
808
+ iterable_item_moved
738
809
values_changed
739
810
type_changes
740
811
set_item_added
@@ -758,15 +829,18 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
758
829
('old_type' , 'old_type' , None ),
759
830
('new_path' , 'new_path' , _parse_path ),
760
831
]
761
- action_mapping = {}
762
832
else :
833
+ if not self .always_include_values :
834
+ raise ValueError (
835
+ "When converting to flat dictionaries, if report_type_changes=False and there are type changes, "
836
+ "you must set the always_include_values=True at the delta object creation. Otherwise there is nothing to include."
837
+ )
763
838
keys_and_funcs = [
764
839
('value' , 'value' , None ),
765
840
('new_value' , 'value' , None ),
766
841
('old_value' , 'old_value' , None ),
767
842
('new_path' , 'new_path' , _parse_path ),
768
843
]
769
- action_mapping = {'type_changes' : 'values_changed' }
770
844
771
845
FLATTENING_NEW_ACTION_MAP = {
772
846
'iterable_items_added_at_indexes' : 'iterable_item_added' ,
@@ -819,9 +893,20 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
819
893
result .append (
820
894
{'path' : path , 'value' : value , 'action' : action }
821
895
)
896
+ elif action == 'type_changes' :
897
+ if not report_type_changes :
898
+ action = 'values_changed'
899
+
900
+ for row in self ._get_flat_row (
901
+ action = action ,
902
+ info = info ,
903
+ _parse_path = _parse_path ,
904
+ keys_and_funcs = keys_and_funcs ,
905
+ ):
906
+ result .append (row )
822
907
else :
823
908
for row in self ._get_flat_row (
824
- action = action_mapping . get ( action , action ) ,
909
+ action = action ,
825
910
info = info ,
826
911
_parse_path = _parse_path ,
827
912
keys_and_funcs = keys_and_funcs ,
0 commit comments