Skip to content

Commit b0a70d3

Browse files
committed
Using difflib to come up with better diff results when order is
important
1 parent e916b5f commit b0a70d3

File tree

9 files changed

+378
-212
lines changed

9 files changed

+378
-212
lines changed

deepdiff/diff.py

Lines changed: 237 additions & 86 deletions
Large diffs are not rendered by default.

deepdiff/helper.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ class np_type:
114114
only_complex_number = (complex,) + numpy_complex_numbers
115115
only_numbers = (int, float, complex, Decimal) + numpy_numbers
116116
datetimes = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time)
117-
uuids = (uuid.UUID)
117+
uuids = (uuid.UUID, )
118118
times = (datetime.datetime, datetime.time)
119119
numbers = only_numbers + datetimes
120120
booleans = (bool, np_bool_)
121121

122+
basic_types = strings + numbers + uuids + booleans + (type(None), )
123+
122124
IndexedHash = namedtuple('IndexedHash', 'indexes item')
123125

124126
current_dir = os.path.dirname(os.path.abspath(__file__))

deepdiff/model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ def __getitem__(self, item):
9696
self[item] = PrettyOrderedSet()
9797
return self.get(item)
9898

99+
def __len__(self):
100+
return sum([len(i) for i in self.values() if isinstance(i, PrettyOrderedSet)])
101+
99102

100103
class TextResult(ResultDict):
101104
ADD_QUOTES_TO_STRINGS = True

tests/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ def parameterize_cases(argnames, cases):
1313
1414
"""
1515
argnames_list = [i.strip() for i in argnames.split(',')]
16-
argvalues = [tuple(i[k] for k in argnames_list) for i in cases.values()]
16+
if 'test_name' not in argnames_list:
17+
argnames_list.append('test_name')
18+
argvalues = [tuple(test_name if (k == 'test_name') else test_dict[k] for k in argnames_list) for test_name, test_dict in cases.items()]
1719
ids = list(cases.keys())
1820
return {'argnames': argnames, 'argvalues': argvalues, 'ids': ids}
1921

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,40 @@
11
{
2-
"dictionary_item_added": [
3-
"root['Cars'][3]['dealers']"
4-
],
5-
"dictionary_item_removed": [
6-
"root['Cars'][3]['production']"
7-
],
8-
"values_changed": {
9-
"root['Cars'][2]['dealers'][0]['quantity']": {
10-
"new_value": 50,
11-
"old_value": 20
12-
},
13-
"root['Cars'][1]['model_numbers'][2]": {
14-
"new_value": 3,
15-
"old_value": 4
16-
},
17-
"root['Cars'][3]['model']": {
18-
"new_value": "Supra",
19-
"old_value": "supra"
20-
}
21-
},
22-
"iterable_item_added": {
23-
"root['Cars'][2]['dealers'][1]": {
24-
"id": 200,
25-
"address": "200 Fake St",
26-
"quantity": 10
2+
"dictionary_item_added": [
3+
"root['Cars'][3]['dealers']"
4+
],
5+
"dictionary_item_removed": [
6+
"root['Cars'][3]['production']"
7+
],
8+
"values_changed": {
9+
"root['Cars'][3]['model']": {
10+
"new_value": "Supra",
11+
"old_value": "supra"
12+
}
2713
},
28-
"root['Cars'][1]['model_numbers'][3]": 4,
29-
"root['Cars'][0]": {
30-
"id": "7",
31-
"make": "Toyota",
32-
"model": "8Runner"
33-
}
34-
},
35-
"iterable_item_removed": {
36-
"root['Cars'][2]['dealers'][0]": {
37-
"id": 103,
38-
"address": "103 Fake St",
39-
"quantity": 50
14+
"iterable_item_added": {
15+
"root['Cars'][0]": {
16+
"id": "7",
17+
"make": "Toyota",
18+
"model": "8Runner"
19+
}
4020
},
41-
"root['Cars'][1]": {
42-
"id": "2",
43-
"make": "Toyota",
44-
"model": "Highlander",
45-
"dealers": [
46-
{
47-
"id": 123,
48-
"address": "123 Fake St",
49-
"quantity": 50
50-
},
51-
{
52-
"id": 125,
53-
"address": "125 Fake St",
54-
"quantity": 20
21+
"iterable_item_removed": {
22+
"root['Cars'][1]": {
23+
"id": "2",
24+
"make": "Toyota",
25+
"model": "Highlander",
26+
"dealers": [
27+
{
28+
"id": 123,
29+
"address": "123 Fake St",
30+
"quantity": 50
31+
},
32+
{
33+
"id": 125,
34+
"address": "125 Fake St",
35+
"quantity": 20
36+
}
37+
]
5538
}
56-
]
5739
}
58-
}
5940
}

tests/test_delta.py

Lines changed: 62 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -634,21 +634,28 @@ def test_delta_dict_items_added_retain_order(self):
634634
'to_delta_kwargs': {},
635635
'expected_delta_dict': {'iterable_item_removed': {'root[9]': 'a', 'root[10]': 'b', 'root[11]': 'c'}}
636636
},
637+
'delta_case19_value_removed_from_the_middle_of_list': {
638+
't1': [0, 1, 2, 3, 4, 5, 6, 7, 8, 'a', 'b', 'c'],
639+
't2': [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c'],
640+
'deepdiff_kwargs': {},
641+
'to_delta_kwargs': {'directed': True},
642+
'expected_delta_dict': {'iterable_item_removed': {'root[4]': 4}}
643+
},
637644
}
638645

639646

640-
DELTA_CASES_PARAMS = parameterize_cases('t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict', DELTA_CASES)
647+
DELTA_CASES_PARAMS = parameterize_cases('test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict', DELTA_CASES)
641648

642649

643650
class TestDelta:
644651

645652
@pytest.mark.parametrize(**DELTA_CASES_PARAMS)
646-
def test_delta_cases(self, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict):
653+
def test_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict):
647654
diff = DeepDiff(t1, t2, **deepdiff_kwargs)
648655
delta_dict = diff._to_delta_dict(**to_delta_kwargs)
649-
assert expected_delta_dict == delta_dict
656+
assert expected_delta_dict == delta_dict, f"test_delta_cases {test_name} failed."
650657
delta = Delta(diff, verify_symmetry=False, raise_errors=True)
651-
assert t1 + delta == t2
658+
assert t1 + delta == t2, f"test_delta_cases {test_name} failed."
652659

653660

654661
DELTA_IGNORE_ORDER_CASES = {
@@ -931,15 +938,15 @@ def test_delta_cases(self, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_de
931938
}
932939

933940
DELTA_IGNORE_ORDER_CASES_PARAMS = parameterize_cases(
934-
't1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta', DELTA_IGNORE_ORDER_CASES)
941+
'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta', DELTA_IGNORE_ORDER_CASES)
935942

936943

937944
class TestIgnoreOrderDelta:
938945

939946
@pytest.mark.parametrize(**DELTA_IGNORE_ORDER_CASES_PARAMS)
940947
def test_ignore_order_delta_cases(
941-
self, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta, request):
942-
test_name = request.node.callspec.id
948+
self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_t1_plus_delta, request):
949+
# test_name = request.node.callspec.id
943950
diff = DeepDiff(t1, t2, **deepdiff_kwargs)
944951
delta_dict = diff._to_delta_dict(**to_delta_kwargs)
945952
assert expected_delta_dict == delta_dict, f"test_ignore_order_delta_cases {test_name} failed"
@@ -1094,31 +1101,31 @@ def test_ignore_order_delta_cases(
10941101

10951102

10961103
DELTA_NUMPY_TEST_PARAMS = parameterize_cases(
1097-
't1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result', DELTA_NUMPY_TEST_CASES)
1104+
'test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result', DELTA_NUMPY_TEST_CASES)
10981105

10991106

11001107
class TestNumpyDelta:
11011108

11021109
@pytest.mark.parametrize(**DELTA_NUMPY_TEST_PARAMS)
1103-
def test_numpy_delta_cases(self, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result):
1110+
def test_numpy_delta_cases(self, test_name, t1, t2, deepdiff_kwargs, to_delta_kwargs, expected_delta_dict, expected_result):
11041111
diff = DeepDiff(t1, t2, **deepdiff_kwargs)
11051112
delta_dict = diff._to_delta_dict(**to_delta_kwargs)
11061113
if expected_delta_dict:
1107-
assert expected_delta_dict == delta_dict
1114+
assert expected_delta_dict == delta_dict, f"test_numpy_delta_cases {test_name} failed."
11081115
delta = Delta(diff, verify_symmetry=False, raise_errors=True)
11091116
if expected_result == 't2':
11101117
result = delta + t1
1111-
assert np.array_equal(result, t2)
1118+
assert np.array_equal(result, t2), f"test_numpy_delta_cases {test_name} failed."
11121119
elif expected_result == 't2_via_deepdiff':
11131120
result = delta + t1
11141121
diff = DeepDiff(result, t2, ignore_order=True, report_repetition=True)
1115-
assert not diff
1122+
assert not diff, f"test_numpy_delta_cases {test_name} failed."
11161123
elif expected_result is DeltaNumpyOperatorOverrideError:
11171124
with pytest.raises(DeltaNumpyOperatorOverrideError):
1118-
assert t1 + delta
1125+
t1 + delta
11191126
else:
11201127
result = delta + t1
1121-
assert np.array_equal(result, expected_result)
1128+
assert np.array_equal(result, expected_result), f"test_numpy_delta_cases {test_name} failed."
11221129

11231130
def test_invalid_numpy_type(self):
11241131

@@ -1510,11 +1517,27 @@ def test_compare_func_with_duplicates_removed(self):
15101517
t2 = [{'id': 3, 'val': 3}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}]
15111518
ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
15121519
expected = {
1513-
'values_changed': {"root[2]['val']": {'new_value': 3, 'old_value': 1}},
1514-
'iterable_item_removed': {'root[2]': {'id': 1, 'val': 3}},
1515-
'iterable_item_moved': {
1516-
'root[0]': {'new_path': 'root[2]', 'value': {'id': 1, 'val': 3}},
1517-
'root[3]': {'new_path': 'root[0]', 'value': {'id': 3, 'val': 3}}
1520+
"iterable_item_removed": {
1521+
"root[2]": {
1522+
"id": 1,
1523+
"val": 3
1524+
}
1525+
},
1526+
"iterable_item_moved": {
1527+
"root[0]": {
1528+
"new_path": "root[2]",
1529+
"value": {
1530+
"id": 1,
1531+
"val": 3
1532+
}
1533+
},
1534+
"root[3]": {
1535+
"new_path": "root[0]",
1536+
"value": {
1537+
"id": 3,
1538+
"val": 3
1539+
}
1540+
}
15181541
}
15191542
}
15201543
assert expected == ddiff
@@ -1527,11 +1550,27 @@ def test_compare_func_with_duplicates_added(self):
15271550
t2 = [{'id': 1, 'val': 1}, {'id': 2, 'val': 2}, {'id': 1, 'val': 3}, {'id': 3, 'val': 3}]
15281551
ddiff = DeepDiff(t1, t2, iterable_compare_func=self.compare_func, verbose_level=2)
15291552
expected = {
1530-
'values_changed': {"root[0]['val']": {'new_value': 1, 'old_value': 3}},
1531-
'iterable_item_added': {'root[2]': {'id': 1, 'val': 3}},
1553+
'iterable_item_added': {
1554+
'root[2]': {
1555+
'id': 1,
1556+
'val': 3
1557+
}
1558+
},
15321559
'iterable_item_moved': {
1533-
'root[2]': {'new_path': 'root[0]', 'value': {'id': 1, 'val': 1}},
1534-
'root[0]': {'new_path': 'root[3]', 'value': {'id': 3, 'val': 3}}
1560+
'root[0]': {
1561+
'new_path': 'root[3]',
1562+
'value': {
1563+
'id': 3,
1564+
'val': 3
1565+
}
1566+
},
1567+
'root[2]': {
1568+
'new_path': 'root[0]',
1569+
'value': {
1570+
'id': 1,
1571+
'val': 1
1572+
}
1573+
}
15351574
}
15361575
}
15371576
assert expected == ddiff

tests/test_diff_numpy.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,12 @@
132132
}
133133

134134

135-
NUMPY_CASES_PARAMS = parameterize_cases('t1, t2, deepdiff_kwargs, expected_result', NUMPY_CASES)
135+
NUMPY_CASES_PARAMS = parameterize_cases('test_name, t1, t2, deepdiff_kwargs, expected_result', NUMPY_CASES)
136136

137137

138138
class TestNumpy:
139139

140140
@pytest.mark.parametrize(**NUMPY_CASES_PARAMS)
141-
def test_numpy(self, t1, t2, deepdiff_kwargs, expected_result):
141+
def test_numpy(self, test_name, t1, t2, deepdiff_kwargs, expected_result):
142142
diff = DeepDiff(t1, t2, **deepdiff_kwargs)
143-
assert expected_result == diff
143+
assert expected_result == diff, f"test_numpy {test_name} failed."

tests/test_diff_text.py

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -426,44 +426,14 @@ def test_list_difference3(self):
426426
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}}
427427
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}}
428428
ddiff = DeepDiff(t1, t2)
429-
result = {
430-
'values_changed': {
431-
"root[4]['b'][2]": {
432-
'new_value': 2,
433-
'old_value': 5
434-
},
435-
"root[4]['b'][1]": {
436-
'new_value': 3,
437-
'old_value': 2
438-
}
439-
},
440-
'iterable_item_added': {
441-
"root[4]['b'][3]": 5
442-
}
443-
}
444-
assert result == ddiff
429+
expected = {'iterable_item_added': {"root[4]['b'][1]": 3}}
430+
assert expected == ddiff
445431

446432
def test_list_difference4(self):
447-
# TODO: Look into Levenshtein algorithm
448-
# So that the result is just insertion of "c" in this test.
449433
t1 = ["a", "b", "d", "e"]
450434
t2 = ["a", "b", "c", "d", "e"]
451435
ddiff = DeepDiff(t1, t2)
452-
result = {
453-
'values_changed': {
454-
'root[2]': {
455-
'new_value': 'c',
456-
'old_value': 'd'
457-
},
458-
'root[3]': {
459-
'new_value': 'd',
460-
'old_value': 'e'
461-
}
462-
},
463-
'iterable_item_added': {
464-
'root[4]': 'e'
465-
}
466-
}
436+
result = {'iterable_item_added': {'root[2]': 'c'}}
467437
assert result == ddiff
468438

469439
def test_list_of_booleans(self):
@@ -1502,6 +1472,35 @@ def test_list_none_item_removed(self):
15021472
assert result == ddiff
15031473
assert {"root[2]"} == ddiff.affected_paths
15041474

1475+
def test_list_item_removed_from_the_middle(self):
1476+
t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c']
1477+
t2 = [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c']
1478+
diff = DeepDiff(t1, t2)
1479+
result = {'iterable_item_removed': {'root[4]': 'bye'}}
1480+
assert result == diff
1481+
assert {"root[4]"} == diff.affected_paths
1482+
assert {4} == diff.affected_root_keys
1483+
1484+
def test_list_item_values_replace_in_the_middle(self):
1485+
t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c']
1486+
t2 = [0, 1, 2, 3, 'see', 'you', 'later', 5, 6, 7, 8, 'a', 'b', 'c']
1487+
diff = DeepDiff(t1, t2)
1488+
result = {
1489+
'values_changed': {
1490+
'root[4]': {
1491+
'old_value': 'bye',
1492+
'new_value': 'see',
1493+
}
1494+
},
1495+
'iterable_item_added': {
1496+
'root[5]': 'you',
1497+
'root[6]': 'later'
1498+
}
1499+
}
1500+
assert result == diff
1501+
assert {'root[5]', 'root[6]', 'root[4]'} == diff.affected_paths
1502+
assert {4, 5, 6} == diff.affected_root_keys
1503+
15051504
def test_non_subscriptable_iterable(self):
15061505
def gen1():
15071506
yield 42

0 commit comments

Comments
 (0)