@@ -71,6 +71,7 @@ def __init__(
71
71
diff = None ,
72
72
delta_path = None ,
73
73
delta_file = None ,
74
+ delta_diff = None ,
74
75
flat_dict_list = None ,
75
76
deserializer = pickle_load ,
76
77
log_errors = True ,
@@ -81,6 +82,7 @@ def __init__(
81
82
verify_symmetry = None ,
82
83
bidirectional = False ,
83
84
always_include_values = False ,
85
+ iterable_compare_func_was_used = None ,
84
86
force = False ,
85
87
):
86
88
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):
114
116
with open (delta_path , 'rb' ) as the_file :
115
117
content = the_file .read ()
116
118
self .diff = _deserializer (content , safe_to_import = safe_to_import )
119
+ elif delta_diff :
120
+ self .diff = delta_diff
117
121
elif delta_file :
118
122
try :
119
123
content = delta_file .read ()
@@ -128,7 +132,10 @@ def _deserializer(obj, safe_to_import=None):
128
132
self .mutate = mutate
129
133
self .raise_errors = raise_errors
130
134
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 )
132
139
self .serializer = serializer
133
140
self .deserializer = deserializer
134
141
self .force = force
@@ -198,7 +205,17 @@ def _do_verify_changes(self, path, expected_old_value, current_old_value):
198
205
self ._raise_or_log (VERIFICATION_MSG .format (
199
206
path_str , expected_old_value , current_old_value , VERIFY_BIDIRECTIONAL_MSG ))
200
207
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:
202
219
try :
203
220
if action == GET :
204
221
current_old_value = obj [elem ]
@@ -208,9 +225,21 @@ def _get_elem_and_compare_to_old_value(self, obj, path_for_err_reporting, expect
208
225
raise DeltaError (INVALID_ACTION_WHEN_CALLING_GET_ELEM .format (action ))
209
226
except (KeyError , IndexError , AttributeError , TypeError ) as e :
210
227
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
212
235
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
214
243
elif action == GETATTR :
215
244
setattr (obj , elem , _forced_old_value )
216
245
return _forced_old_value
@@ -277,6 +306,11 @@ def _set_new_value(self, parent, parent_to_obj_elem, parent_to_obj_action,
277
306
parent , obj , path , parent_to_obj_elem ,
278
307
parent_to_obj_action , elements ,
279
308
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 )
280
314
self ._simple_set_elem_value (obj = obj , path_for_err_reporting = path , elem = elem ,
281
315
value = new_value , action = action )
282
316
@@ -404,14 +438,21 @@ def _get_elements_and_details(self, path):
404
438
try :
405
439
elements = _path_to_elements (path )
406
440
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 )
408
448
parent_to_obj_elem , parent_to_obj_action = elements [- 2 ]
409
449
obj = self ._get_elem_and_compare_to_old_value (
410
450
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 )
412
452
else :
413
453
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])
415
456
elem , action = elements [- 1 ]
416
457
except Exception as e :
417
458
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
458
499
self ._do_verify_changes (path , expected_old_value , current_old_value )
459
500
460
501
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 ):
461
551
"""
462
552
Handle removing items.
463
553
"""
@@ -695,10 +785,9 @@ def _from_flat_dicts(flat_dict_list):
695
785
Create the delta's diff object from the flat_dict_list
696
786
"""
697
787
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' ,
702
791
}
703
792
for flat_dict in flat_dict_list :
704
793
index = None
@@ -710,8 +799,8 @@ def _from_flat_dicts(flat_dict_list):
710
799
raise ValueError ("Flat dict need to include the 'action'." )
711
800
if path is None :
712
801
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 ]
715
804
index = path .pop ()
716
805
if action in {'attribute_added' , 'attribute_removed' }:
717
806
root_element = ('root' , GETATTR )
@@ -729,8 +818,8 @@ def _from_flat_dicts(flat_dict_list):
729
818
result [action ][path_str ] = set ()
730
819
result [action ][path_str ].add (value )
731
820
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' ,
734
823
}:
735
824
result [action ][path_str ] = value
736
825
elif action == 'values_changed' :
@@ -843,10 +932,12 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
843
932
]
844
933
845
934
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 ' ,
848
937
}
849
938
for action , info in self .diff .items ():
939
+ if action .startswith ('_' ):
940
+ continue
850
941
if action in FLATTENING_NEW_ACTION_MAP :
851
942
new_action = FLATTENING_NEW_ACTION_MAP [action ]
852
943
for path , index_to_value in info .items ():
0 commit comments