@@ -71,6 +71,7 @@ def __init__(
7171 diff = None ,
7272 delta_path = None ,
7373 delta_file = None ,
74+ delta_diff = None ,
7475 flat_dict_list = None ,
7576 deserializer = pickle_load ,
7677 log_errors = True ,
@@ -81,6 +82,7 @@ def __init__(
8182 verify_symmetry = None ,
8283 bidirectional = False ,
8384 always_include_values = False ,
85+ iterable_compare_func_was_used = None ,
8486 force = False ,
8587 ):
8688 if hasattr (deserializer , '__code__' ) and 'safe_to_import' in set (deserializer .__code__ .co_varnames ):
@@ -114,6 +116,8 @@ def _deserializer(obj, safe_to_import=None):
114116 with open (delta_path , 'rb' ) as the_file :
115117 content = the_file .read ()
116118 self .diff = _deserializer (content , safe_to_import = safe_to_import )
119+ elif delta_diff :
120+ self .diff = delta_diff
117121 elif delta_file :
118122 try :
119123 content = delta_file .read ()
@@ -128,7 +132,10 @@ def _deserializer(obj, safe_to_import=None):
128132 self .mutate = mutate
129133 self .raise_errors = raise_errors
130134 self .log_errors = log_errors
131- self ._numpy_paths = self .diff .pop ('_numpy_paths' , False )
135+ self ._numpy_paths = self .diff .get ('_numpy_paths' , False )
136+ # When we create the delta from a list of flat dictionaries, details such as iterable_compare_func_was_used get lost.
137+ # That's why we allow iterable_compare_func_was_used to be explicitly set.
138+ self ._iterable_compare_func_was_used = self .diff .get ('_iterable_compare_func_was_used' , iterable_compare_func_was_used )
132139 self .serializer = serializer
133140 self .deserializer = deserializer
134141 self .force = force
@@ -198,7 +205,17 @@ def _do_verify_changes(self, path, expected_old_value, current_old_value):
198205 self ._raise_or_log (VERIFICATION_MSG .format (
199206 path_str , expected_old_value , current_old_value , VERIFY_BIDIRECTIONAL_MSG ))
200207
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 ):
208+ def _get_elem_and_compare_to_old_value (
209+ self ,
210+ obj ,
211+ path_for_err_reporting ,
212+ expected_old_value ,
213+ elem = None ,
214+ action = None ,
215+ forced_old_value = None ,
216+ next_element = None ,
217+ ):
218+ # if forced_old_value is not None:
202219 try :
203220 if action == GET :
204221 current_old_value = obj [elem ]
@@ -208,9 +225,21 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
208225 raise DeltaError (INVALID_ACTION_WHEN_CALLING_GET_ELEM .format (action ))
209226 except (KeyError , IndexError , AttributeError , TypeError ) as e :
210227 if self .force :
211- _forced_old_value = {} if forced_old_value is None else forced_old_value
228+ if forced_old_value is None :
229+ if next_element is None or isinstance (next_element , str ):
230+ _forced_old_value = {}
231+ else :
232+ _forced_old_value = []
233+ else :
234+ _forced_old_value = forced_old_value
212235 if action == GET :
213- obj [elem ] = _forced_old_value
236+ if isinstance (obj , list ):
237+ if isinstance (elem , int ) and elem < len (obj ):
238+ obj [elem ] = _forced_old_value
239+ else :
240+ obj .append (_forced_old_value )
241+ else :
242+ obj [elem ] = _forced_old_value
214243 elif action == GETATTR :
215244 setattr (obj , elem , _forced_old_value )
216245 return _forced_old_value
@@ -277,6 +306,11 @@ def _set_new_value(self, parent, parent_to_obj_elem, parent_to_obj_action,
277306 parent , obj , path , parent_to_obj_elem ,
278307 parent_to_obj_action , elements ,
279308 to_type = list , from_type = tuple )
309+ if elem != 0 and self .force and isinstance (obj , list ) and len (obj ) == 0 :
310+ # it must have been a dictionary
311+ obj = {}
312+ self ._simple_set_elem_value (obj = parent , path_for_err_reporting = path , elem = parent_to_obj_elem ,
313+ value = obj , action = parent_to_obj_action )
280314 self ._simple_set_elem_value (obj = obj , path_for_err_reporting = path , elem = elem ,
281315 value = new_value , action = action )
282316
@@ -404,14 +438,21 @@ def _get_elements_and_details(self, path):
404438 try :
405439 elements = _path_to_elements (path )
406440 if len (elements ) > 1 :
407- parent = self .get_nested_obj (obj = self , elements = elements [:- 2 ])
441+ elements_subset = elements [:- 2 ]
442+ if len (elements_subset ) != len (elements ):
443+ next_element = elements [- 2 ][0 ]
444+ next2_element = elements [- 1 ][0 ]
445+ else :
446+ next_element = None
447+ parent = self .get_nested_obj (obj = self , elements = elements_subset , next_element = next_element )
408448 parent_to_obj_elem , parent_to_obj_action = elements [- 2 ]
409449 obj = self ._get_elem_and_compare_to_old_value (
410450 obj = parent , path_for_err_reporting = path , expected_old_value = None ,
411- elem = parent_to_obj_elem , action = parent_to_obj_action )
451+ elem = parent_to_obj_elem , action = parent_to_obj_action , next_element = next2_element )
412452 else :
413453 parent = parent_to_obj_elem = parent_to_obj_action = None
414- obj = self .get_nested_obj (obj = self , elements = elements [:- 1 ])
454+ obj = self
455+ # obj = self.get_nested_obj(obj=self, elements=elements[:-1])
415456 elem , action = elements [- 1 ]
416457 except Exception as e :
417458 self ._raise_or_log (UNABLE_TO_GET_ITEM_MSG .format (path , e ))
@@ -458,6 +499,55 @@ def _do_values_or_type_changed(self, changes, is_type_change=False, verify_chang
458499 self ._do_verify_changes (path , expected_old_value , current_old_value )
459500
460501 def _do_item_removed (self , items ):
502+ """
503+ Handle removing items.
504+ """
505+ # Sorting the iterable_item_removed in reverse order based on the paths.
506+ # So that we delete a bigger index before a smaller index
507+ for path , expected_old_value in sorted (items .items (), key = self ._sort_key_for_item_added , reverse = True ):
508+ elem_and_details = self ._get_elements_and_details (path )
509+ if elem_and_details :
510+ elements , parent , parent_to_obj_elem , parent_to_obj_action , obj , elem , action = elem_and_details
511+ else :
512+ continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
513+
514+ look_for_expected_old_value = False
515+ current_old_value = not_found
516+ try :
517+ if action == GET :
518+ current_old_value = obj [elem ]
519+ look_for_expected_old_value = current_old_value != expected_old_value
520+ elif action == GETATTR :
521+ current_old_value = getattr (obj , elem )
522+ look_for_expected_old_value = current_old_value != expected_old_value
523+ except (KeyError , IndexError , AttributeError , TypeError ):
524+ look_for_expected_old_value = True
525+
526+ if look_for_expected_old_value and isinstance (obj , list ) and not self ._iterable_compare_func_was_used :
527+ # It may return None if it doesn't find it
528+ elem = self ._find_closest_iterable_element_for_index (obj , elem , expected_old_value )
529+ if elem is not None :
530+ current_old_value = expected_old_value
531+ if current_old_value is not_found or elem is None :
532+ continue
533+
534+ self ._del_elem (parent , parent_to_obj_elem , parent_to_obj_action ,
535+ obj , elements , path , elem , action )
536+ self ._do_verify_changes (path , expected_old_value , current_old_value )
537+
538+ def _find_closest_iterable_element_for_index (self , obj , elem , expected_old_value ):
539+ closest_elem = None
540+ closest_distance = float ('inf' )
541+ for index , value in enumerate (obj ):
542+ dist = abs (index - elem )
543+ if dist > closest_distance :
544+ break
545+ if value == expected_old_value and dist < closest_distance :
546+ closest_elem = index
547+ closest_distance = dist
548+ return closest_elem
549+
550+ def _do_item_removedOLD (self , items ):
461551 """
462552 Handle removing items.
463553 """
@@ -695,10 +785,9 @@ def _from_flat_dicts(flat_dict_list):
695785 Create the delta's diff object from the flat_dict_list
696786 """
697787 result = {}
698-
699- DEFLATTENING_NEW_ACTION_MAP = {
700- 'iterable_item_added' : 'iterable_items_added_at_indexes' ,
701- 'iterable_item_removed' : 'iterable_items_removed_at_indexes' ,
788+ FLATTENING_NEW_ACTION_MAP = {
789+ 'unordered_iterable_item_added' : 'iterable_items_added_at_indexes' ,
790+ 'unordered_iterable_item_removed' : 'iterable_items_removed_at_indexes' ,
702791 }
703792 for flat_dict in flat_dict_list :
704793 index = None
@@ -710,8 +799,8 @@ def _from_flat_dicts(flat_dict_list):
710799 raise ValueError ("Flat dict need to include the 'action'." )
711800 if path is None :
712801 raise ValueError ("Flat dict need to include the 'path'." )
713- if action in DEFLATTENING_NEW_ACTION_MAP :
714- action = DEFLATTENING_NEW_ACTION_MAP [action ]
802+ if action in FLATTENING_NEW_ACTION_MAP :
803+ action = FLATTENING_NEW_ACTION_MAP [action ]
715804 index = path .pop ()
716805 if action in {'attribute_added' , 'attribute_removed' }:
717806 root_element = ('root' , GETATTR )
@@ -729,8 +818,8 @@ def _from_flat_dicts(flat_dict_list):
729818 result [action ][path_str ] = set ()
730819 result [action ][path_str ].add (value )
731820 elif action in {
732- 'dictionary_item_added' , 'dictionary_item_removed' , 'iterable_item_added' ,
733- 'iterable_item_removed ' , 'attribute_removed ' , 'attribute_added'
821+ 'dictionary_item_added' , 'dictionary_item_removed' ,
822+ 'attribute_removed ' , 'attribute_added ' , 'iterable_item_added' , 'iterable_item_removed' ,
734823 }:
735824 result [action ][path_str ] = value
736825 elif action == 'values_changed' :
@@ -843,10 +932,12 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
843932 ]
844933
845934 FLATTENING_NEW_ACTION_MAP = {
846- 'iterable_items_added_at_indexes' : 'iterable_item_added ' ,
847- 'iterable_items_removed_at_indexes' : 'iterable_item_removed ' ,
935+ 'iterable_items_added_at_indexes' : 'unordered_iterable_item_added ' ,
936+ 'iterable_items_removed_at_indexes' : 'unordered_iterable_item_removed ' ,
848937 }
849938 for action , info in self .diff .items ():
939+ if action .startswith ('_' ):
940+ continue
850941 if action in FLATTENING_NEW_ACTION_MAP :
851942 new_action = FLATTENING_NEW_ACTION_MAP [action ]
852943 for path , index_to_value in info .items ():
0 commit comments