Skip to content

Commit 89c5cc2

Browse files
authored
Merge pull request #432 from seperman/dev
6.7.1
2 parents 39c3a3d + db9f667 commit 89c5cc2

16 files changed

+353
-85
lines changed

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# DeepDiff Change log
22

3+
- v6-7-1
4+
- Support for subtracting delta objects when iterable_compare_func is used.
5+
- Better handling of force adding a delta to an object.
6+
- Fix for [`Can't compare dicts with both single and double quotes in keys`](https://github.com/seperman/deepdiff/issues/430)
7+
- Updated docs for Inconsistent Behavior with math_epsilon and ignore_order = True
38
- v6-7-0
49
- Delta can be subtracted from other objects now.
510
- verify_symmetry is deprecated. Use bidirectional instead.

README.md

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# DeepDiff v 6.7.0
1+
# DeepDiff v 6.7.1
22

33
![Downloads](https://img.shields.io/pypi/dm/deepdiff.svg?style=flat)
44
![Python Versions](https://img.shields.io/pypi/pyversions/deepdiff.svg?style=flat)
@@ -17,13 +17,20 @@
1717

1818
Tested on Python 3.7+ and PyPy3.
1919

20-
- **[Documentation](https://zepworks.com/deepdiff/6.7.0/)**
20+
- **[Documentation](https://zepworks.com/deepdiff/6.7.1/)**
2121

2222
## What is new?
2323

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

26-
DeepDiff v6-7-0
26+
DeepDiff 6-7-1
27+
28+
- Support for subtracting delta objects when iterable_compare_func is used.
29+
- Better handling of force adding a delta to an object.
30+
- Fix for [`Can't compare dicts with both single and double quotes in keys`](https://github.com/seperman/deepdiff/issues/430)
31+
- Updated docs for Inconsistent Behavior with math_epsilon and ignore_order = True
32+
33+
DeepDiff 6-7-0
2734

2835
- Delta can be subtracted from other objects now.
2936
- verify_symmetry is deprecated. Use bidirectional instead.
@@ -98,11 +105,11 @@ Thank you!
98105

99106
How to cite this library (APA style):
100107

101-
Dehpour, S. (2023). DeepDiff (Version 6.7.0) [Software]. Available from https://github.com/seperman/deepdiff.
108+
Dehpour, S. (2023). DeepDiff (Version 6.7.1) [Software]. Available from https://github.com/seperman/deepdiff.
102109

103110
How to cite this library (Chicago style):
104111

105-
Dehpour, Sep. 2023. DeepDiff (version 6.7.0).
112+
Dehpour, Sep. 2023. DeepDiff (version 6.7.1).
106113

107114
# Authors
108115

deepdiff/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""This module offers the DeepDiff, DeepSearch, grep, Delta and DeepHash classes."""
22
# flake8: noqa
3-
__version__ = '6.7.0'
3+
__version__ = '6.7.1'
44
import logging
55

66
if __name__ == '__main__':

deepdiff/delta.py

+108-17
Original file line numberDiff line numberDiff line change
@@ -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():

deepdiff/diff.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ def _group_by_sort_key(x):
231231
self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes)
232232
self.math_epsilon = math_epsilon
233233
if self.math_epsilon is not None and self.ignore_order:
234-
logger.warning("math_epsilon will be ignored. It cannot be used when ignore_order is True.")
234+
logger.warning("math_epsilon in conjunction with ignore_order=True is only used for flat object comparisons. Custom math_epsilon will not have an effect when comparing nested objects.")
235235
self.truncate_datetime = get_truncate_datetime(truncate_datetime)
236236
self.number_format_notation = number_format_notation
237237
if verbose_level in {0, 1, 2}:

deepdiff/path.py

+39-10
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def _add_to_elements(elements, elem, inside):
2222
return
2323
if not elem.startswith('__'):
2424
remove_quotes = False
25-
if '\\' in elem:
25+
if '𝆺𝅥𝅯' in elem or '\\' in elem:
2626
remove_quotes = True
2727
else:
2828
try:
@@ -62,7 +62,7 @@ def _path_to_elements(path, root_element=DEFAULT_FIRST_ELEMENT):
6262
inside_quotes = False
6363
quote_used = ''
6464
for char in path:
65-
if prev_char == '\\':
65+
if prev_char == '𝆺𝅥𝅯':
6666
elem += char
6767
elif char in {'"', "'"}:
6868
elem += char
@@ -115,7 +115,7 @@ def _path_to_elements(path, root_element=DEFAULT_FIRST_ELEMENT):
115115
return tuple(elements)
116116

117117

118-
def _get_nested_obj(obj, elements):
118+
def _get_nested_obj(obj, elements, next_element=None):
119119
for (elem, action) in elements:
120120
if action == GET:
121121
obj = obj[elem]
@@ -124,21 +124,50 @@ def _get_nested_obj(obj, elements):
124124
return obj
125125

126126

127-
def _get_nested_obj_and_force(obj, elements):
128-
for (elem, action) in elements:
127+
def _guess_type(elements, elem, index, next_element):
128+
# If we are not at the last elements
129+
if index < len(elements) - 1:
130+
# We assume it is a nested dictionary not a nested list
131+
return {}
132+
if isinstance(next_element, int):
133+
return []
134+
return {}
135+
136+
137+
def _get_nested_obj_and_force(obj, elements, next_element=None):
138+
prev_elem = None
139+
prev_action = None
140+
prev_obj = obj
141+
for index, (elem, action) in enumerate(elements):
142+
_prev_obj = obj
129143
if action == GET:
130144
try:
131145
obj = obj[elem]
146+
prev_obj = _prev_obj
132147
except KeyError:
133-
obj[elem] = {}
148+
obj[elem] = _guess_type(elements, elem, index, next_element)
134149
obj = obj[elem]
150+
prev_obj = _prev_obj
135151
except IndexError:
136152
if isinstance(obj, list) and isinstance(elem, int) and elem >= len(obj):
137153
obj.extend([None] * (elem - len(obj)))
138-
obj.append({})
154+
obj.append(_guess_type(elements, elem, index), next_element)
139155
obj = obj[-1]
156+
prev_obj = _prev_obj
157+
elif isinstance(obj, list) and len(obj) == 0 and prev_elem:
158+
# We ran into an empty list that should have been a dictionary
159+
# We need to change it from an empty list to a dictionary
160+
obj = {elem: _guess_type(elements, elem, index, next_element)}
161+
if prev_action == GET:
162+
prev_obj[prev_elem] = obj
163+
else:
164+
setattr(prev_obj, prev_elem, obj)
165+
obj = obj[elem]
140166
elif action == GETATTR:
141167
obj = getattr(obj, elem)
168+
prev_obj = _prev_obj
169+
prev_elem = elem
170+
prev_action = action
142171
return obj
143172

144173

@@ -241,13 +270,13 @@ def parse_path(path, root_element=DEFAULT_FIRST_ELEMENT, include_actions=False):
241270
def stringify_element(param, quote_str=None):
242271
has_quote = "'" in param
243272
has_double_quote = '"' in param
244-
if has_quote and has_double_quote:
273+
if has_quote and has_double_quote and not quote_str:
245274
new_param = []
246275
for char in param:
247276
if char in {'"', "'"}:
248-
new_param.append('\\')
277+
new_param.append('𝆺𝅥𝅯')
249278
new_param.append(char)
250-
param = ''.join(new_param)
279+
result = '"' + ''.join(new_param) + '"'
251280
elif has_quote:
252281
result = f'"{param}"'
253282
elif has_double_quote:

0 commit comments

Comments
 (0)