2323VERIFICATION_MSG = 'Expected the old value for {} to be {} but it is {}. Error found on: {}'
2424ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.'
2525TYPE_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. ' )
2828FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}'
2929DELTA_NUMPY_OPERATOR_OVERRIDE_MSG = (
3030 'A numpy ndarray is most likely being added to a delta. '
@@ -78,7 +78,9 @@ def __init__(
7878 raise_errors = False ,
7979 safe_to_import = None ,
8080 serializer = pickle_dump ,
81- verify_symmetry = False ,
81+ verify_symmetry = None ,
82+ bidirectional = False ,
83+ always_include_values = False ,
8284 force = False ,
8385 ):
8486 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):
8991
9092 self ._reversed_diff = None
9193
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+
92106 if diff is not None :
93107 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 )
95109 elif isinstance (diff , Mapping ):
96110 self .diff = diff
97111 elif isinstance (diff , strings ):
@@ -112,7 +126,6 @@ def _deserializer(obj, safe_to_import=None):
112126 raise ValueError (DELTA_AT_LEAST_ONE_ARG_NEEDED )
113127
114128 self .mutate = mutate
115- self .verify_symmetry = verify_symmetry
116129 self .raise_errors = raise_errors
117130 self .log_errors = log_errors
118131 self ._numpy_paths = self .diff .pop ('_numpy_paths' , False )
@@ -162,16 +175,28 @@ def __add__(self, other):
162175
163176 __radd__ = __add__
164177
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+
165186 def _raise_or_log (self , msg , level = 'error' ):
166187 if self .log_errors :
167188 getattr (logger , level )(msg )
168189 if self .raise_errors :
169190 raise DeltaError (msg )
170191
171192 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 ))
173198 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 ))
175200
176201 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 ):
177202 try :
@@ -192,7 +217,7 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
192217 current_old_value = not_found
193218 if isinstance (path_for_err_reporting , (list , tuple )):
194219 path_for_err_reporting = '.' .join ([i [0 ] for i in path_for_err_reporting ])
195- if self .verify_symmetry :
220+ if self .bidirectional :
196221 self ._raise_or_log (VERIFICATION_MSG .format (
197222 path_for_err_reporting ,
198223 expected_old_value , current_old_value , e ))
@@ -357,7 +382,9 @@ def _do_type_changes(self):
357382
358383 def _do_post_process (self ):
359384 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 )
361388
362389 def _do_pre_process (self ):
363390 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):
394421 return None
395422 return elements , parent , parent_to_obj_elem , parent_to_obj_action , obj , elem , action
396423
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 ):
398425 for path , value in changes .items ():
399426 elem_and_details = self ._get_elements_and_details (path )
400427 if elem_and_details :
@@ -409,7 +436,7 @@ def _do_values_or_type_changed(self, changes, is_type_change=False):
409436 continue # pragma: no cover. I have not been able to write a test for this case. But we should still check for it.
410437 # With type change if we could have originally converted the type from old_value
411438 # 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
413440 if is_type_change and 'new_value' not in value :
414441 try :
415442 new_type = value ['new_type' ]
@@ -427,7 +454,8 @@ def _do_values_or_type_changed(self, changes, is_type_change=False):
427454 self ._set_new_value (parent , parent_to_obj_elem , parent_to_obj_action ,
428455 obj , elements , path , elem , action , new_value )
429456
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 )
431459
432460 def _do_item_removed (self , items ):
433461 """
@@ -580,8 +608,50 @@ def _do_ignore_order(self):
580608 self ._simple_set_elem_value (obj = parent , path_for_err_reporting = path , elem = parent_to_obj_elem ,
581609 value = new_obj , action = parent_to_obj_action )
582610
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
585655
586656 def dump (self , file ):
587657 """
@@ -735,6 +805,7 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
735805 Here are the list of actions that the flat dictionary can return.
736806 iterable_item_added
737807 iterable_item_removed
808+ iterable_item_moved
738809 values_changed
739810 type_changes
740811 set_item_added
@@ -758,15 +829,18 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
758829 ('old_type' , 'old_type' , None ),
759830 ('new_path' , 'new_path' , _parse_path ),
760831 ]
761- action_mapping = {}
762832 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+ )
763838 keys_and_funcs = [
764839 ('value' , 'value' , None ),
765840 ('new_value' , 'value' , None ),
766841 ('old_value' , 'old_value' , None ),
767842 ('new_path' , 'new_path' , _parse_path ),
768843 ]
769- action_mapping = {'type_changes' : 'values_changed' }
770844
771845 FLATTENING_NEW_ACTION_MAP = {
772846 '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):
819893 result .append (
820894 {'path' : path , 'value' : value , 'action' : action }
821895 )
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 )
822907 else :
823908 for row in self ._get_flat_row (
824- action = action_mapping . get ( action , action ) ,
909+ action = action ,
825910 info = info ,
826911 _parse_path = _parse_path ,
827912 keys_and_funcs = keys_and_funcs ,
0 commit comments