Skip to content

Commit b88455b

Browse files
committed
subtract delta
1 parent 48c4944 commit b88455b

File tree

11 files changed

+320
-108
lines changed

11 files changed

+320
-108
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# DeepDiff Change log
22

3+
- v6-7-0
4+
- Delta can be subtracted from other objects now.
5+
- verify_symmetry is deprecated. Use bidirectional instead.
6+
- always_include_values flag in Delta can be enabled to include values in the delta for every change.
7+
- Fix for Delta.__add__ breaks with esoteric dict keys.
8+
- You can load a delta from the list of flat dictionaries.
39
- v6-6-1
410
- Fix for [DeepDiff raises decimal exception when using significant digits](https://github.com/seperman/deepdiff/issues/426)
511
- Introducing group_by_sort_key

README.md

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,21 @@ Tested on Python 3.7+ and PyPy3.
2323

2424
Please check the [ChangeLog](CHANGELOG.md) file for the detailed information.
2525

26+
DeepDiff v6-7-0
27+
28+
- Delta can be subtracted from other objects now.
29+
- verify_symmetry is deprecated. Use bidirectional instead.
30+
- always_include_values flag in Delta can be enabled to include values in the delta for every change.
31+
- Fix for Delta.__add__ breaks with esoteric dict keys.
32+
- You can load a delta from the list of flat dictionaries.
33+
2634
DeepDiff 6-6-1
35+
2736
- Fix for [DeepDiff raises decimal exception when using significant digits](https://github.com/seperman/deepdiff/issues/426)
2837
- Introducing group_by_sort_key
2938
- Adding group_by 2D. For example `group_by=['last_name', 'zip_code']`
3039

3140

32-
DeepDiff 6-6-0
33-
34-
- [Serialize To Flat Dicts](https://zepworks.com/deepdiff/current/serialization.html#delta-to-flat-dicts-label)
35-
- [NumPy 2.0 compatibility](https://github.com/seperman/deepdiff/pull/422) by [William Jamieson](https://github.com/WilliamJamieson)
36-
37-
DeepDiff 6-5-0
38-
39-
- [parse_path](https://zepworks.com/deepdiff/current/faq.html#q-how-do-i-parse-deepdiff-result-paths)
40-
41-
DeepDiff 6-4-1
42-
43-
- [Add Ignore List Order Option to DeepHash](https://github.com/seperman/deepdiff/pull/403) by
44-
[Bobby Morck](https://github.com/bmorck)
45-
- [pyyaml to 6.0.1 to fix cython build problems](https://github.com/seperman/deepdiff/pull/406) by [Robert Bo Davis](https://github.com/robert-bo-davis)
46-
- [Precompiled regex simple diff](https://github.com/seperman/deepdiff/pull/413) by [cohml](https://github.com/cohml)
47-
- New flag: `zip_ordered_iterables` for forcing iterable items to be compared one by one.
48-
49-
5041
## Installation
5142

5243
### Install from PyPi:

deepdiff/delta.py

Lines changed: 102 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
VERIFICATION_MSG = 'Expected the old value for {} to be {} but it is {}. Error found on: {}'
2424
ELEM_NOT_FOUND_TO_ADD_MSG = 'Key or index of {} is not found for {} for setting operation.'
2525
TYPE_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.')
2828
FAIL_TO_REMOVE_ITEM_IGNORE_ORDER_MSG = 'Failed to remove index[{}] on {}. It was expected to be {} but got {}'
2929
DELTA_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,

deepdiff/diff.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,10 +493,9 @@ def _skip_this(self, level):
493493
elif self.include_obj_callback_strict and level_path != 'root':
494494
skip = True
495495
if (self.include_obj_callback_strict(level.t1, level_path) and
496-
self.include_obj_callback_strict(level.t2, level_path)):
496+
self.include_obj_callback_strict(level.t2, level_path)):
497497
skip = False
498498

499-
500499
return skip
501500

502501
def _get_clean_to_keys_mapping(self, keys, level):

deepdiff/model.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,9 @@ def _from_tree_custom_results(self, tree):
279279
class DeltaResult(TextResult):
280280
ADD_QUOTES_TO_STRINGS = False
281281

282-
def __init__(self, tree_results=None, ignore_order=None):
282+
def __init__(self, tree_results=None, ignore_order=None, always_include_values=False):
283283
self.ignore_order = ignore_order
284+
self.always_include_values = always_include_values
284285

285286
self.update({
286287
"type_changes": dict_(),
@@ -375,7 +376,7 @@ def _from_tree_type_changes(self, tree):
375376
})
376377
self['type_changes'][change.path(
377378
force=FORCE_DEFAULT)] = remap_dict
378-
if include_values:
379+
if include_values or self.always_include_values:
379380
remap_dict.update(old_value=change.t1, new_value=change.t2)
380381

381382
def _from_tree_value_changed(self, tree):

deepdiff/serialization.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def to_dict(self, view_override=None):
215215
view = view_override if view_override else self.view
216216
return dict(self._get_view_results(view))
217217

218-
def _to_delta_dict(self, directed=True, report_repetition_required=True):
218+
def _to_delta_dict(self, directed=True, report_repetition_required=True, always_include_values=False):
219219
"""
220220
Dump to a dictionary suitable for delta usage.
221221
Unlike to_dict, this is not dependent on the original view that the user chose to create the diff.
@@ -241,7 +241,7 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True):
241241
if self.group_by is not None:
242242
raise ValueError(DELTA_ERROR_WHEN_GROUP_BY)
243243

244-
result = DeltaResult(tree_results=self.tree, ignore_order=self.ignore_order)
244+
result = DeltaResult(tree_results=self.tree, ignore_order=self.ignore_order, always_include_values=always_include_values)
245245
result.remove_empty_keys()
246246
if report_repetition_required and self.ignore_order and not self.report_repetition:
247247
raise ValueError(DELTA_IGNORE_ORDER_NEEDS_REPETITION_REPORT)

docs/changelog.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ Changelog
55

66
DeepDiff Changelog
77

8+
- v6-7-0
9+
10+
- Delta can be subtracted from other objects now.
11+
- verify_symmetry is deprecated. Use bidirectional instead.
12+
- always_include_values flag in Delta can be enabled to include
13+
values in the delta for every change.
14+
- Fix for Delta.\__add\_\_ breaks with esoteric dict keys.
15+
816
- v6-6-1
917

1018
- Fix for `DeepDiff raises decimal exception when using significant

0 commit comments

Comments
 (0)