@@ -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
@@ -356,6 +390,9 @@ def _do_item_added(self, items, sort=True, insert=False):
356
390
else :
357
391
items = items .items ()
358
392
393
+ # if getattr(self, 'DEBUG', None):
394
+ # import pytest; pytest.set_trace()
395
+
359
396
for path , new_value in items :
360
397
elem_and_details = self ._get_elements_and_details (path )
361
398
if elem_and_details :
@@ -404,14 +441,21 @@ def _get_elements_and_details(self, path):
404
441
try :
405
442
elements = _path_to_elements (path )
406
443
if len (elements ) > 1 :
407
- parent = self .get_nested_obj (obj = self , elements = elements [:- 2 ])
444
+ elements_subset = elements [:- 2 ]
445
+ if len (elements_subset ) != len (elements ):
446
+ next_element = elements [- 2 ][0 ]
447
+ next2_element = elements [- 1 ][0 ]
448
+ else :
449
+ next_element = None
450
+ parent = self .get_nested_obj (obj = self , elements = elements_subset , next_element = next_element )
408
451
parent_to_obj_elem , parent_to_obj_action = elements [- 2 ]
409
452
obj = self ._get_elem_and_compare_to_old_value (
410
453
obj = parent , path_for_err_reporting = path , expected_old_value = None ,
411
- elem = parent_to_obj_elem , action = parent_to_obj_action )
454
+ elem = parent_to_obj_elem , action = parent_to_obj_action , next_element = next2_element )
412
455
else :
413
456
parent = parent_to_obj_elem = parent_to_obj_action = None
414
- obj = self .get_nested_obj (obj = self , elements = elements [:- 1 ])
457
+ obj = self
458
+ # obj = self.get_nested_obj(obj=self, elements=elements[:-1])
415
459
elem , action = elements [- 1 ]
416
460
except Exception as e :
417
461
self ._raise_or_log (UNABLE_TO_GET_ITEM_MSG .format (path , e ))
@@ -458,6 +502,57 @@ def _do_values_or_type_changed(self, changes, is_type_change=False, verify_chang
458
502
self ._do_verify_changes (path , expected_old_value , current_old_value )
459
503
460
504
def _do_item_removed (self , items ):
505
+ """
506
+ Handle removing items.
507
+ """
508
+ # Sorting the iterable_item_removed in reverse order based on the paths.
509
+ # So that we delete a bigger index before a smaller index
510
+ # if hasattr(self, 'DEBUG'):
511
+ # import pytest; pytest.set_trace()
512
+ for path , expected_old_value in sorted (items .items (), key = self ._sort_key_for_item_added , reverse = True ):
513
+ elem_and_details = self ._get_elements_and_details (path )
514
+ if elem_and_details :
515
+ elements , parent , parent_to_obj_elem , parent_to_obj_action , obj , elem , action = elem_and_details
516
+ else :
517
+ continue # pragma: no cover. Due to cPython peephole optimizer, this line doesn't get covered. https://github.com/nedbat/coveragepy/issues/198
518
+
519
+ look_for_expected_old_value = False
520
+ current_old_value = not_found
521
+ try :
522
+ if action == GET :
523
+ current_old_value = obj [elem ]
524
+ look_for_expected_old_value = current_old_value != expected_old_value
525
+ elif action == GETATTR :
526
+ current_old_value = getattr (obj , elem )
527
+ look_for_expected_old_value = current_old_value != expected_old_value
528
+ except (KeyError , IndexError , AttributeError , TypeError ):
529
+ look_for_expected_old_value = True
530
+
531
+ if look_for_expected_old_value and isinstance (obj , list ) and not self ._iterable_compare_func_was_used :
532
+ # It may return None if it doesn't find it
533
+ elem = self ._find_closest_iterable_element_for_index (obj , elem , expected_old_value )
534
+ if elem is not None :
535
+ current_old_value = expected_old_value
536
+ if current_old_value is not_found or elem is None :
537
+ continue
538
+
539
+ self ._del_elem (parent , parent_to_obj_elem , parent_to_obj_action ,
540
+ obj , elements , path , elem , action )
541
+ self ._do_verify_changes (path , expected_old_value , current_old_value )
542
+
543
+ def _find_closest_iterable_element_for_index (self , obj , elem , expected_old_value ):
544
+ closest_elem = None
545
+ closest_distance = float ('inf' )
546
+ for index , value in enumerate (obj ):
547
+ dist = abs (index - elem )
548
+ if dist > closest_distance :
549
+ break
550
+ if value == expected_old_value and dist < closest_distance :
551
+ closest_elem = index
552
+ closest_distance = dist
553
+ return closest_elem
554
+
555
+ def _do_item_removedOLD (self , items ):
461
556
"""
462
557
Handle removing items.
463
558
"""
@@ -695,10 +790,9 @@ def _from_flat_dicts(flat_dict_list):
695
790
Create the delta's diff object from the flat_dict_list
696
791
"""
697
792
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' ,
793
+ FLATTENING_NEW_ACTION_MAP = {
794
+ 'unordered_iterable_item_added' : 'iterable_items_added_at_indexes' ,
795
+ 'unordered_iterable_item_removed' : 'iterable_items_removed_at_indexes' ,
702
796
}
703
797
for flat_dict in flat_dict_list :
704
798
index = None
@@ -710,8 +804,8 @@ def _from_flat_dicts(flat_dict_list):
710
804
raise ValueError ("Flat dict need to include the 'action'." )
711
805
if path is None :
712
806
raise ValueError ("Flat dict need to include the 'path'." )
713
- if action in DEFLATTENING_NEW_ACTION_MAP :
714
- action = DEFLATTENING_NEW_ACTION_MAP [action ]
807
+ if action in FLATTENING_NEW_ACTION_MAP :
808
+ action = FLATTENING_NEW_ACTION_MAP [action ]
715
809
index = path .pop ()
716
810
if action in {'attribute_added' , 'attribute_removed' }:
717
811
root_element = ('root' , GETATTR )
@@ -729,8 +823,8 @@ def _from_flat_dicts(flat_dict_list):
729
823
result [action ][path_str ] = set ()
730
824
result [action ][path_str ].add (value )
731
825
elif action in {
732
- 'dictionary_item_added' , 'dictionary_item_removed' , 'iterable_item_added' ,
733
- 'iterable_item_removed ' , 'attribute_removed ' , 'attribute_added'
826
+ 'dictionary_item_added' , 'dictionary_item_removed' ,
827
+ 'attribute_removed ' , 'attribute_added ' , 'iterable_item_added' , 'iterable_item_removed' ,
734
828
}:
735
829
result [action ][path_str ] = value
736
830
elif action == 'values_changed' :
@@ -843,10 +937,12 @@ def to_flat_dicts(self, include_action_in_path=False, report_type_changes=True):
843
937
]
844
938
845
939
FLATTENING_NEW_ACTION_MAP = {
846
- 'iterable_items_added_at_indexes' : 'iterable_item_added ' ,
847
- 'iterable_items_removed_at_indexes' : 'iterable_item_removed ' ,
940
+ 'iterable_items_added_at_indexes' : 'unordered_iterable_item_added ' ,
941
+ 'iterable_items_removed_at_indexes' : 'unordered_iterable_item_removed ' ,
848
942
}
849
943
for action , info in self .diff .items ():
944
+ if action .startswith ('_' ):
945
+ continue
850
946
if action in FLATTENING_NEW_ACTION_MAP :
851
947
new_action = FLATTENING_NEW_ACTION_MAP [action ]
852
948
for path , index_to_value in info .items ():
0 commit comments