Skip to content

Commit a349699

Browse files
committed
1. Nested namedtuple set/frozenset updates could replace the whole result with the inner
namedtuple, dropping the outer container. Fixed by updating the namedtuple in its actual parent when nested, while preserving root-level namedtuple behavior. 2. Tuple deltas using iterable opcodes could silently do nothing for insert/delete-only changes. Fixed by writing the transformed tuple back instead of reconstructing the original tuple. 3. Applying a delta with both moved and added iterable items could mutate the delta’s own internal diff data. Fixed by copying the added-items mapping before inserting temporary move placeholders. 4. Removing multiple dictionary items with complex keys could crash during path sorting. Fixed by correcting the None check and falling back to string comparison when same-type path elements are still not orderable. Regression tests were added for each case, and the full Delta test suite passes.
1 parent 2db8427 commit a349699

2 files changed

Lines changed: 52 additions & 13 deletions

File tree

deepdiff/delta.py

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def _del_elem(self, parent, parent_to_obj_elem, parent_to_obj_action,
391391
value=obj, action=parent_to_obj_action)
392392

393393
def _do_iterable_item_added(self):
394-
iterable_item_added = self.diff.get('iterable_item_added', {})
394+
iterable_item_added = dict(self.diff.get('iterable_item_added', {}))
395395
iterable_item_moved = self.diff.get('iterable_item_moved')
396396

397397
# First we need to create a placeholder for moved items.
@@ -448,7 +448,7 @@ def _sort_comparison(left, right):
448448
elif len(right_path) > len(left_path):
449449
right_path = right_path[:len(left_path)]
450450
for l_elem, r_elem in zip(left_path, right_path):
451-
if type(l_elem) != type(r_elem) or type(l_elem) in None:
451+
if type(l_elem) != type(r_elem) or l_elem is None or r_elem is None:
452452
l_elem = str(l_elem)
453453
r_elem = str(r_elem)
454454
try:
@@ -457,7 +457,12 @@ def _sort_comparison(left, right):
457457
elif l_elem > r_elem:
458458
return 1
459459
except TypeError:
460-
continue
460+
l_elem = str(l_elem)
461+
r_elem = str(r_elem)
462+
if l_elem < r_elem:
463+
return -1
464+
elif l_elem > r_elem:
465+
return 1
461466
return 0
462467

463468

@@ -677,7 +682,7 @@ def _do_iterable_opcodes(self):
677682
# Items are the same in both lists, so we add them to the result
678683
transformed.extend(obj[opcode.t1_from_index:opcode.t1_to_index]) # type: ignore
679684
if is_obj_tuple:
680-
obj = tuple(obj) # type: ignore
685+
obj = tuple(transformed) # type: ignore
681686
# Making sure that the object is re-instated inside the parent especially if it was immutable
682687
# and we had to turn it into a mutable one. In such cases the object has a new id.
683688
self._simple_set_elem_value(obj=parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
@@ -725,18 +730,24 @@ def _do_set_item_removed(self):
725730

726731
def _do_set_or_frozenset_item(self, items, func):
727732
for path, value in items.items():
728-
elements = _path_to_elements(path)
729-
parent = self.get_nested_obj(obj=self, elements=elements[:-1])
730-
elem, action = elements[-1]
733+
elem_and_details = self._get_elements_and_details(path)
734+
if not elem_and_details:
735+
continue
736+
elements, parent, parent_to_obj_elem, parent_to_obj_action, obj, elem, action = elem_and_details
731737
obj = self._get_elem_and_compare_to_old_value(
732-
parent, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set())
738+
obj, path_for_err_reporting=path, expected_old_value=None, elem=elem, action=action, forced_old_value=set())
733739
new_value = getattr(obj, func)(value)
734-
if hasattr(parent, '_fields') and hasattr(parent, '_replace'):
735-
# Handle parent NamedTuple by creating a new instance with _replace(). Will not work with nested objects.
736-
new_parent = parent._replace(**{elem: new_value})
737-
self.root = new_parent
740+
set_parent = self.get_nested_obj(obj=self, elements=elements[:-1])
741+
replace = getattr(set_parent, '_replace', None)
742+
if hasattr(set_parent, '_fields') and callable(replace):
743+
new_parent = replace(**{elem: new_value})
744+
if parent is None:
745+
self.root = new_parent
746+
else:
747+
self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=parent_to_obj_elem,
748+
value=new_parent, action=parent_to_obj_action)
738749
else:
739-
self._simple_set_elem_value(parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action)
750+
self._simple_set_elem_value(set_parent, path_for_err_reporting=path, elem=elem, value=new_value, action=action)
740751

741752
def _do_ignore_order_get_old(self, obj, remove_indexes_per_path, fixed_indexes_values, path_for_err_reporting):
742753
"""

tests/test_delta.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,32 @@ class Article(NamedTuple):
645645
diff = DeepDiff(a1, a2)
646646
delta = Delta(diff)
647647
assert a2 == a1 + delta
648+
649+
def test_nested_namedtuple_frozenset_add_delta(self):
650+
class Article(NamedTuple):
651+
tags: frozenset
652+
653+
t1 = {"article": Article(frozenset(["a"]))}
654+
t2 = {"article": Article(frozenset(["a", "b"]))}
655+
delta = Delta(DeepDiff(t1, t2))
656+
657+
assert t2 == t1 + delta
658+
659+
def test_tuple_iterable_opcodes_with_insert_delete_delta(self):
660+
t1 = tuple("A B C D H".split())
661+
t2 = tuple("B C D H Y Z".split())
662+
delta = Delta(DeepDiff(t1, t2), bidirectional=True)
663+
664+
assert "_iterable_opcodes" in delta.diff
665+
assert t2 == t1 + delta
666+
667+
def test_complex_dictionary_keys_removed_delta(self):
668+
t1 = {1 + 2j: "a", 3 + 4j: "b"}
669+
t2 = {}
670+
diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
671+
delta = Delta(diff, raise_errors=True)
672+
673+
assert t2 == t1 + delta
648674

649675
picklalbe_obj_without_item = PicklableClass(11)
650676
del picklalbe_obj_without_item.item
@@ -2133,8 +2159,10 @@ def test_compare_func_with_duplicates_added(self):
21332159
}
21342160
assert expected == ddiff
21352161
delta = Delta(ddiff)
2162+
flat_rows_before_apply = delta.to_flat_rows()
21362163
recreated_t2 = t1 + delta
21372164
assert t2 == recreated_t2
2165+
assert flat_rows_before_apply == delta.to_flat_rows()
21382166

21392167
def test_compare_func_swap(self):
21402168
t1 = [{'id': 1, 'val': 1}, {'id': 1, 'val': 3}]

0 commit comments

Comments
 (0)