Skip to content

Commit 96847f2

Browse files
committed
adding zip_ordered_iterables
1 parent 0cf607d commit 96847f2

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

deepdiff/diff.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def __init__(self,
142142
ignore_type_in_groups=None,
143143
ignore_type_subclasses=False,
144144
iterable_compare_func=None,
145+
zip_ordered_iterables=False,
145146
log_frequency_in_sec=0,
146147
math_epsilon=None,
147148
max_diffs=None,
@@ -166,7 +167,7 @@ def __init__(self,
166167
"number_format_notation, exclude_paths, include_paths, exclude_types, exclude_regex_paths, ignore_type_in_groups, "
167168
"ignore_string_type_changes, ignore_numeric_type_changes, ignore_type_subclasses, truncate_datetime, "
168169
"ignore_private_variables, ignore_nan_inequality, number_to_string_func, verbose_level, "
169-
"view, hasher, hashes, max_passes, max_diffs, "
170+
"view, hasher, hashes, max_passes, max_diffs, zip_ordered_iterables, "
170171
"cutoff_distance_for_pairs, cutoff_intersection_for_pairs, log_frequency_in_sec, cache_size, "
171172
"cache_tuning_sample_size, get_deep_distance, group_by, cache_purge_level, "
172173
"math_epsilon, iterable_compare_func, _original_type, "
@@ -208,6 +209,7 @@ def __init__(self,
208209
self.include_obj_callback_strict = include_obj_callback_strict
209210
self.number_to_string = number_to_string_func or number_to_string
210211
self.iterable_compare_func = iterable_compare_func
212+
self.zip_ordered_iterables = zip_ordered_iterables
211213
self.ignore_private_variables = ignore_private_variables
212214
self.ignore_nan_inequality = ignore_nan_inequality
213215
self.hasher = hasher
@@ -742,7 +744,8 @@ def _diff_iterable_in_order(self, level, parents_ids=frozenset(), _original_type
742744
child_relationship_class = NonSubscriptableIterableRelationship
743745

744746
if (
745-
isinstance(level.t1, Sequence)
747+
not self.zip_ordered_iterables
748+
and isinstance(level.t1, Sequence)
746749
and isinstance(level.t2, Sequence)
747750
and self._all_values_basic_hashable(level.t1)
748751
and self._all_values_basic_hashable(level.t2)

docs/diff_doc.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ ignore_encoding_errors: Boolean, default = False
129129
:ref:`ignore_encoding_errors_label` If you want to get away with UnicodeDecodeError without passing explicit character encodings, set this option to True. If you want to make sure the encoding is done properly, keep this as False and instead pass an explicit list of character encodings to be considered via the :ref:`encodings_label` parameter.
130130

131131

132+
zip_ordered_iterables: Boolean, default = False
133+
:ref:`zip_ordered_iterables_label`:
134+
When comparing ordered iterables such as lists, DeepDiff tries to find the smallest difference between the two iterables to report. That means that items in the two lists are not paired individually in the order of appearance in the iterables. Sometimes, that is not the desired behavior. Set this flag to True to make DeepDiff pair and compare the items in the iterables in the order they appear.
135+
132136
iterable_compare_func:
133137
:ref:`iterable_compare_func_label`:
134138
There are times that we want to guide DeepDiff as to what items to compare with other items. In such cases we can pass a iterable_compare_func that takes a function pointer to compare two items. The function takes three parameters (x, y, level) and should return True if it is a match, False if it is not a match or raise CannotCompare if it is unable to compare the two.

docs/optimizations.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ cache_purge_level: int, 0, 1, or 2. default=1
241241
cache_purge_level defines what objects in DeepDiff should be deleted to free the memory once the diff object is calculated. If this value is set to zero, most of the functionality of the diff object is removed and the most memory is released. A value of 1 preserves all the functionalities of the diff object. A value of 2 also preserves the cache and hashes that were calculated during the diff calculations. In most cases the user does not need to have those objects remained in the diff unless for investigation purposes.
242242

243243

244+
.. _zip_ordered_iterables_label:
245+
246+
Zip Ordered Iterables
247+
---------------------
248+
249+
zip_ordered_iterables: Boolean, default = False
250+
When comparing ordered iterables such as lists, DeepDiff tries to find the smallest difference between the two iterables to report. That means that items in the two lists are not paired individually in the order of appearance in the iterables. Sometimes, that is not the desired behavior. Set this flag to True to make DeepDiff pair and compare the items in the iterables in the order they appear.
251+
252+
253+
>>> from pprint import pprint
254+
>>> from deepdiff import DeepDiff
255+
>>> t1 = ["a", "b", "d", "e"]
256+
>>> t2 = ["a", "b", "c", "d", "e"]
257+
>>> DeepDiff(t1, t2)
258+
{'iterable_item_added': {'root[2]': 'c'}}
259+
260+
When this flag is set to True and ignore_order=False, diffing will be faster.
261+
262+
>>> diff=DeepDiff(t1, t2, zip_ordered_iterables=True)
263+
>>> pprint(diff)
264+
{'iterable_item_added': {'root[4]': 'e'},
265+
'values_changed': {'root[2]': {'new_value': 'c', 'old_value': 'd'},
266+
'root[3]': {'new_value': 'd', 'old_value': 'e'}}}
244267

245268

246269

tests/test_diff_text.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,60 @@ def test_list_difference4(self):
438438
result = {'iterable_item_added': {'root[2]': 'c'}}
439439
assert result == ddiff
440440

441+
def test_list_difference5(self):
442+
t1 = ["a", "b", "d", "e", "f", "g"]
443+
t2 = ["a", "b", "c", "d", "e", "f"]
444+
ddiff = DeepDiff(t1, t2)
445+
result = {'iterable_item_added': {'root[2]': 'c'}, 'iterable_item_removed': {'root[5]': 'g'}}
446+
assert result == ddiff
447+
448+
def test_list_difference_with_tiny_variations(self):
449+
t1 = ['a', 'b', 'c', 'd']
450+
t2 = ['f', 'b', 'a', 'g']
451+
452+
values = {
453+
'a': 2.0000000000000027,
454+
'b': 2.500000000000005,
455+
'c': 2.000000000000002,
456+
'd': 3.000000000000001,
457+
'f': 2.000000000000003,
458+
'g': 3.0000000000000027,
459+
}
460+
ddiff = DeepDiff(t1, t2)
461+
result = {
462+
'values_changed': {
463+
'root[0]': {
464+
'new_value': 'f',
465+
'old_value': 'a'
466+
},
467+
'root[2]': {
468+
'new_value': 'a',
469+
'old_value': 'c'
470+
},
471+
'root[3]': {
472+
'new_value': 'g',
473+
'old_value': 'd'
474+
}
475+
}
476+
}
477+
assert result == ddiff
478+
479+
ddiff2 = DeepDiff(t1, t2, zip_ordered_iterables=True)
480+
assert result == ddiff2
481+
# Now we change the characters with numbers with tiny variations
482+
483+
t3 = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001]
484+
t4 = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027]
485+
ddiff3 = DeepDiff(t3, t4)
486+
487+
expected = {'values_changed': {}}
488+
for path, report in result['values_changed'].items():
489+
expected['values_changed'][path] = {
490+
'new_value': values[report['new_value']],
491+
'old_value': values[report['old_value']],
492+
}
493+
assert expected == ddiff3
494+
441495
def test_list_of_booleans(self):
442496
t1 = [False, False, True, True]
443497
t2 = [False, False, False, True]
@@ -1803,4 +1857,3 @@ class Bar(PydanticBaseModel):
18031857
diff = DeepDiff(t1, t2)
18041858
expected = {'values_changed': {'root.stuff[0].thing': {'new_value': 2, 'old_value': 1}}}
18051859
assert expected == diff
1806-

tests/test_operators.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,3 +240,40 @@ def test_prefix_or_suffix_diff(self):
240240

241241
expected2 = {'values_changed': {"root['key1'][2]": {'new_value': 'jill', 'old_value': 'jack'}}}
242242
assert expected2 == ddiff2
243+
244+
def test_custom_operator3_small_numbers(self):
245+
x = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001]
246+
y = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027]
247+
result = DeepDiff(x, y)
248+
expected = {
249+
'values_changed': {
250+
'root[0]': {'new_value': 2.000000000000003, 'old_value': 2.0000000000000027},
251+
'root[2]': {'new_value': 2.0000000000000027, 'old_value': 2.000000000000002},
252+
'root[3]': {'new_value': 3.0000000000000027, 'old_value': 3.000000000000001}}}
253+
assert expected == result
254+
255+
class CustomCompare(BaseOperator):
256+
def __init__(self, tolerance, types):
257+
self.tolerance = tolerance
258+
self.types = types
259+
260+
def match(self, level) -> bool:
261+
if type(level.t1) in self.types:
262+
return True
263+
264+
def give_up_diffing(self, level, diff_instance) -> bool:
265+
relative = abs(abs(level.t1 - level.t2) / level.t1)
266+
if not max(relative, self.tolerance) == self.tolerance:
267+
custom_report = f'relative diff: {relative:.8e}'
268+
diff_instance.custom_report_result('diff', level, custom_report)
269+
return True
270+
271+
def compare_func(x, y, level):
272+
return True
273+
274+
operators = [CustomCompare(types=[float], tolerance=5.5e-5)]
275+
result2 = DeepDiff(x, y, custom_operators=operators, iterable_compare_func=compare_func)
276+
assert {} == result2
277+
278+
result3 = DeepDiff(x, y, custom_operators=operators, zip_ordered_iterables=True)
279+
assert {} == result3, "We should get the same result as result2 when zip_ordered_iterables is True."

0 commit comments

Comments
 (0)