The shortcut in _diff_dict that reports a whole dictionary as a single values_changed entry compensates its key count for exclude_paths and exclude_glob_paths, but never for exclude_regex_paths. Regex-excluded keys are not removed from the key sets beforehand either, since _skip_this_key only filters on include_paths and include_glob_paths. They therefore inflate the key union, drag the ratio of shared keys under the threshold and trigger the shortcut, so the exclusion is silently ignored and the excluded keys leak into old_value and new_value. Filter the key union by exclude_regex_paths as well, mirroring the exclude_glob_paths branch and using search() to match how _skip_this applies the same patterns. Fixes #487
2300 lines
76 KiB
Python
2300 lines
76 KiB
Python
#!/usr/bin/env python
|
|
import datetime
|
|
import pytest
|
|
import logging
|
|
import re
|
|
import uuid
|
|
from enum import Enum
|
|
from dataclasses import dataclass
|
|
from typing import List
|
|
from decimal import Decimal
|
|
from deepdiff import DeepDiff
|
|
from deepdiff.helper import pypy3, PydanticBaseModel, np_float64
|
|
from tests import CustomClass
|
|
|
|
|
|
logging.disable(logging.CRITICAL)
|
|
|
|
|
|
class MyEnum1(Enum):
|
|
book = "book"
|
|
cake = "cake"
|
|
|
|
class MyEnum2(str, Enum):
|
|
book = "book"
|
|
cake = "cake"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MyDataClass:
|
|
val: int
|
|
val2: int
|
|
|
|
|
|
class TestDeepDiffText:
|
|
"""DeepDiff Tests."""
|
|
|
|
def test_same_objects(self):
|
|
t1 = {1: 1, 2: 2, 3: 3}
|
|
t2 = t1
|
|
assert {} == DeepDiff(t1, t2)
|
|
|
|
def test_item_type_change(self):
|
|
t1 = {1: 1, 2: 2, 3: 3}
|
|
t2 = {1: 1, 2: "2", 3: 3}
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert {
|
|
'type_changes': {
|
|
"root[2]": {
|
|
"old_value": 2,
|
|
"old_type": int,
|
|
"new_value": "2",
|
|
"new_type": str
|
|
}
|
|
}
|
|
} == ddiff
|
|
|
|
def test_item_type_change_less_verbose(self):
|
|
t1 = {1: 1, 2: 2, 3: 3}
|
|
t2 = {1: 1, 2: "2", 3: 3}
|
|
assert {'type_changes': {
|
|
"root[2]": {
|
|
"old_type": int,
|
|
"new_type": str
|
|
}
|
|
}} == DeepDiff(t1, t2, verbose_level=0)
|
|
|
|
def test_item_type_change_for_strings_ignored_by_default(self):
|
|
""" ignore_string_type_changes = True by default """
|
|
|
|
t1 = 'hello'
|
|
t2 = b'hello'
|
|
ddiff = DeepDiff(t1, t2, ignore_string_type_changes=True)
|
|
assert not ddiff
|
|
|
|
def test_item_type_change_for_strings_override(self):
|
|
t1 = 'hello'
|
|
t2 = b'hello'
|
|
ddiff = DeepDiff(t1, t2, ignore_string_type_changes=False)
|
|
assert {
|
|
'type_changes': {
|
|
'root': {
|
|
'old_type': str,
|
|
'new_type': bytes,
|
|
'old_value': 'hello',
|
|
'new_value': b'hello'
|
|
}
|
|
}
|
|
} == ddiff
|
|
|
|
def test_value_change(self):
|
|
t1 = {1: 1, 2: 2, 3: 3}
|
|
t2 = {1: 1, 2: 4, 3: 3}
|
|
result = {
|
|
'values_changed': {
|
|
'root[2]': {
|
|
"old_value": 2,
|
|
"new_value": 4
|
|
}
|
|
}
|
|
}
|
|
assert result == DeepDiff(t1, t2)
|
|
|
|
def test_item_added_and_removed(self):
|
|
t1 = {1: 1, 2: 2, 3: [3], 4: 4}
|
|
t2 = {1: 1, 2: 4, 3: [3, 4], 5: 5, 6: 6}
|
|
ddiff = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
|
|
result = {
|
|
'dictionary_item_added': ["root[5]", "root[6]"],
|
|
'dictionary_item_removed': ["root[4]"],
|
|
'values_changed': {
|
|
'root[2]': {
|
|
'new_value': 4,
|
|
'old_value': 2
|
|
}
|
|
},
|
|
'iterable_item_added': {
|
|
'root[3][1]': 4
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
assert {'root[4]', 'root[5]', 'root[6]', 'root[3][1]', 'root[2]'} == ddiff.affected_paths
|
|
assert {4, 5, 6, 3, 2} == ddiff.affected_root_keys
|
|
|
|
def test_item_added_and_removed_verbose(self):
|
|
t1 = {1: 1, 3: 3, 4: 4}
|
|
t2 = {1: 1, 3: 3, 5: 5, 6: 6}
|
|
ddiff = DeepDiff(t1, t2, verbose_level=2)
|
|
result = {
|
|
'dictionary_item_removed': {
|
|
'root[4]': 4
|
|
},
|
|
'dictionary_item_added': {
|
|
'root[6]': 6,
|
|
'root[5]': 5
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_dates(self):
|
|
t1 = datetime.date(2016, 8, 8)
|
|
t2 = datetime.date(2016, 8, 7)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_timedeltas(self):
|
|
t1 = datetime.timedelta(days=1, seconds=12)
|
|
t2 = datetime.timedelta(days=1, seconds=10)
|
|
t3 = datetime.timedelta(seconds=(60*60*24) + 12)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t3)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_times(self):
|
|
t1 = datetime.time(1, 1)
|
|
t2 = datetime.time(1, 2)
|
|
t3 = datetime.time(1, 1)
|
|
expected_result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert DeepDiff(t1, t2) == expected_result
|
|
assert DeepDiff(t1, t3) == {}
|
|
|
|
def test_diffs_uuid1(self):
|
|
t1 = uuid.uuid1()
|
|
t2 = uuid.uuid1()
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t1)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_uuid3(self):
|
|
t1 = uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org')
|
|
t2 = uuid.uuid3(uuid.NAMESPACE_DNS, 'stackoverflow.com')
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t1)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_uuid4(self):
|
|
t1 = uuid.uuid4()
|
|
t2 = uuid.uuid4()
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t1)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_uuid5(self):
|
|
t1 = uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
|
|
t2 = uuid.uuid5(uuid.NAMESPACE_DNS, 'stackoverflow.com')
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': t2,
|
|
'old_value': t1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t1)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_string_difference(self):
|
|
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world"}}
|
|
t2 = {1: 1, 2: 4, 3: 3, 4: {"a": "hello", "b": "world!"}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[2]': {
|
|
'old_value': 2,
|
|
'new_value': 4
|
|
},
|
|
"root[4]['b']": {
|
|
'old_value': 'world',
|
|
'new_value': 'world!'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_diffs_equal_strings_when_not_identical(self):
|
|
t1 = 'hello'
|
|
t2 = 'hel'
|
|
t2 += 'lo'
|
|
assert t1 is not t2
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert {} == ddiff
|
|
|
|
def test_string_difference2(self):
|
|
t1 = {
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: {
|
|
"a": "hello",
|
|
"b": "world!\nGoodbye!\n1\n2\nEnd"
|
|
}
|
|
}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n1\n2\nEnd"}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
"root[4]['b']": {
|
|
'diff':
|
|
'--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End',
|
|
'new_value': 'world\n1\n2\nEnd',
|
|
'old_value': 'world!\nGoodbye!\n1\n2\nEnd'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_string_difference_ignore_case(self):
|
|
t1 = "Hello"
|
|
t2 = "hello"
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'values_changed': {'root': {'new_value': 'hello', 'old_value': 'Hello'}}}
|
|
assert result == ddiff
|
|
|
|
ddiff = DeepDiff(t1, t2, ignore_string_case=True)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_string_dict_key_ignore_case(self):
|
|
t1 = {'User': {'AboutMe': 1, 'ALIAS': 1}}
|
|
t2 = {'User': {'Alias': 1, 'AboutMe': 1}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'dictionary_item_added': ["root['User']['Alias']"], 'dictionary_item_removed': ["root['User']['ALIAS']"]}
|
|
assert result == ddiff
|
|
|
|
ddiff = DeepDiff(t1, t2, ignore_string_case=True)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_string_list_ignore_case(self):
|
|
t1 = ['AboutMe', 'ALIAS']
|
|
t2 = ['aboutme', 'alias']
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'values_changed': {'root[0]': {'new_value': 'aboutme', 'old_value': 'AboutMe'}, 'root[1]': {'new_value': 'alias', 'old_value': 'ALIAS'}}}
|
|
assert result == ddiff
|
|
|
|
ddiff = DeepDiff(t1, t2, ignore_string_case=True)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_diff_quote_in_string(self):
|
|
t1 = {
|
|
"a']['b']['c": 1
|
|
}
|
|
t2 = {
|
|
"a']['b']['c": 2
|
|
}
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {'''root["a']['b']['c"]''': {'new_value': 2, 'old_value': 1}}}
|
|
assert expected == diff
|
|
|
|
def test_diff_quote_and_double_quote_in_string(self):
|
|
t1 = {'''a'"a''': 1}
|
|
t2 = {'''a'"a''': 2}
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {'root["a\'"a"]': {'new_value': 2, 'old_value': 1}}}
|
|
assert expected == diff
|
|
|
|
def test_bytes(self):
|
|
t1 = {
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: {
|
|
"a": b"hello",
|
|
"b": b"world!\nGoodbye!\n1\n2\nEnd",
|
|
"c": b"\x80",
|
|
}
|
|
}
|
|
t2 = {
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: {
|
|
"a": b"hello",
|
|
"b": b"world\n1\n2\nEnd",
|
|
"c": b'\x81',
|
|
}
|
|
}
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
"root[4]['b']": {
|
|
'diff':
|
|
'--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End',
|
|
'new_value': b'world\n1\n2\nEnd',
|
|
'old_value': b'world!\nGoodbye!\n1\n2\nEnd'
|
|
},
|
|
"root[4]['c']": {
|
|
'new_value': b'\x81',
|
|
'old_value': b'\x80'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_unicode(self):
|
|
t1 = {
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: {
|
|
"a": "hello",
|
|
"b": "world!\nGoodbye!\n1\n2\nEnd"
|
|
}
|
|
}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n1\n2\nEnd"}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
"root[4]['b']": {
|
|
'diff':
|
|
'--- \n+++ \n@@ -1,5 +1,4 @@\n-world!\n-Goodbye!\n+world\n 1\n 2\n End',
|
|
'new_value': 'world\n1\n2\nEnd',
|
|
'old_value': 'world!\nGoodbye!\n1\n2\nEnd'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_type_change(self):
|
|
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3]}}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": "world\n\n\nEnd"}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'type_changes': {
|
|
"root[4]['b']": {
|
|
'old_type': list,
|
|
'new_value': 'world\n\n\nEnd',
|
|
'old_value': [1, 2, 3],
|
|
'new_type': str
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_list_difference(self):
|
|
t1 = {
|
|
1: 1,
|
|
2: 2,
|
|
3: 3,
|
|
4: {
|
|
"a": "hello",
|
|
"b": [1, 2, 'to_be_removed', 'to_be_removed2']
|
|
}
|
|
}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2]}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'iterable_item_removed': {
|
|
"root[4]['b'][2]": "to_be_removed",
|
|
"root[4]['b'][3]": 'to_be_removed2'
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_list_difference_add(self):
|
|
t1 = [1, 2]
|
|
t2 = [1, 2, 3, 5]
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'iterable_item_added': {'root[2]': 3, 'root[3]': 5}}
|
|
assert result == ddiff
|
|
|
|
def test_list_difference2(self):
|
|
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 3, 10, 12]}}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2]}}
|
|
result = {
|
|
'values_changed': {
|
|
"root[4]['b'][2]": {
|
|
'new_value': 2,
|
|
'old_value': 3
|
|
},
|
|
"root[4]['b'][1]": {
|
|
'new_value': 3,
|
|
'old_value': 2
|
|
}
|
|
},
|
|
'iterable_item_removed': {
|
|
"root[4]['b'][3]": 10,
|
|
"root[4]['b'][4]": 12
|
|
}
|
|
}
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert result == ddiff
|
|
|
|
def test_list_difference3(self):
|
|
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, 5]}}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 3, 2, 5]}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
expected = {'iterable_item_added': {"root[4]['b'][1]": 3}}
|
|
assert expected == ddiff
|
|
|
|
def test_list_difference4(self):
|
|
t1 = ["a", "b", "d", "e"]
|
|
t2 = ["a", "b", "c", "d", "e"]
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'iterable_item_added': {'root[2]': 'c'}}
|
|
assert result == ddiff
|
|
|
|
def test_list_difference5(self):
|
|
t1 = ["a", "b", "d", "e", "f", "g"]
|
|
t2 = ["a", "b", "c", "d", "e", "f"]
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'iterable_item_added': {'root[2]': 'c'}, 'iterable_item_removed': {'root[5]': 'g'}}
|
|
assert result == ddiff
|
|
|
|
def test_list_difference_with_tiny_variations(self):
|
|
t1 = ['a', 'b', 'c', 'd']
|
|
t2 = ['f', 'b', 'a', 'g']
|
|
|
|
values = {
|
|
'a': 2.0000000000000027,
|
|
'b': 2.500000000000005,
|
|
'c': 2.000000000000002,
|
|
'd': 3.000000000000001,
|
|
'f': 2.000000000000003,
|
|
'g': 3.0000000000000027,
|
|
}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[0]': {
|
|
'new_value': 'f',
|
|
'old_value': 'a'
|
|
},
|
|
'root[2]': {
|
|
'new_value': 'a',
|
|
'old_value': 'c'
|
|
},
|
|
'root[3]': {
|
|
'new_value': 'g',
|
|
'old_value': 'd'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
ddiff2 = DeepDiff(t1, t2, zip_ordered_iterables=True)
|
|
assert result == ddiff2
|
|
# Now we change the characters with numbers with tiny variations
|
|
|
|
t3 = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001]
|
|
t4 = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027]
|
|
ddiff3 = DeepDiff(t3, t4)
|
|
|
|
expected = {'values_changed': {}}
|
|
for path, report in result['values_changed'].items():
|
|
expected['values_changed'][path] = {
|
|
'new_value': values[report['new_value']],
|
|
'old_value': values[report['old_value']],
|
|
}
|
|
assert expected == ddiff3
|
|
|
|
def test_list_of_booleans(self):
|
|
t1 = [False, False, True, True]
|
|
t2 = [False, False, False, True]
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert {'values_changed': {'root[2]': {'new_value': False, 'old_value': True}}} == ddiff
|
|
|
|
def test_set_of_none(self):
|
|
"""
|
|
https://github.com/seperman/deepdiff/issues/64
|
|
"""
|
|
ddiff = DeepDiff(set(), {None})
|
|
assert {'set_item_added': {'root[None]'}} == ddiff
|
|
|
|
def test_list_that_contains_dictionary(self):
|
|
t1 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, {1: 1, 2: 2}]}}
|
|
t2 = {1: 1, 2: 2, 3: 3, 4: {"a": "hello", "b": [1, 2, {1: 3}]}}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'dictionary_item_removed': {"root[4]['b'][2][2]"},
|
|
'values_changed': {
|
|
"root[4]['b'][2][1]": {
|
|
'old_value': 1,
|
|
'new_value': 3
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_set(self):
|
|
t1 = {1, 2, 8}
|
|
t2 = {1, 2, 3, 5}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'set_item_added': {'root[3]', 'root[5]'},
|
|
'set_item_removed': {'root[8]'}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_set_strings(self):
|
|
t1 = {"veggies", "tofu"}
|
|
t2 = {"veggies", "tofu", "seitan"}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {'set_item_added': {"root['seitan']"}}
|
|
assert result == ddiff
|
|
|
|
def test_frozenset(self):
|
|
t1 = frozenset([1, 2, 'B'])
|
|
t2 = frozenset([1, 2, 3, 5])
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'set_item_added': {'root[3]', 'root[5]'},
|
|
'set_item_removed': {"root['B']"}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_tuple(self):
|
|
t1 = (1, 2, 8)
|
|
t2 = (1, 2, 3, 5)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[2]': {
|
|
'old_value': 8,
|
|
'new_value': 3
|
|
}
|
|
},
|
|
'iterable_item_added': {
|
|
'root[3]': 5
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_named_tuples(self):
|
|
from collections import namedtuple
|
|
Point = namedtuple('Point', ['x', 'y'])
|
|
t1 = Point(x=11, y=22)
|
|
t2 = Point(x=11, y=23)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.y': {
|
|
'old_value': 22,
|
|
'new_value': 23
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_enums(self):
|
|
|
|
class MyEnum(Enum):
|
|
A = 1
|
|
B = 2
|
|
|
|
ddiff = DeepDiff(MyEnum.A, MyEnum(1))
|
|
result = {}
|
|
assert ddiff == result
|
|
|
|
ddiff = DeepDiff(MyEnum.A, MyEnum.B)
|
|
result = {
|
|
'values_changed': {
|
|
'root.name': {
|
|
'old_value': 'A',
|
|
'new_value': 'B'
|
|
},
|
|
'root.value': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
},
|
|
}
|
|
}
|
|
assert ddiff == result
|
|
|
|
def test_enum_ignore_type_change(self):
|
|
|
|
diff = DeepDiff("book", MyEnum1.book)
|
|
expected = {
|
|
'type_changes': {'root': {'old_type': str, 'new_type': MyEnum1, 'old_value': 'book', 'new_value': MyEnum1.book}}}
|
|
assert expected == diff
|
|
|
|
diff2 = DeepDiff("book", MyEnum1.book, ignore_type_in_groups=[(Enum, str)])
|
|
assert not diff2
|
|
|
|
diff3 = DeepDiff("book", MyEnum2.book, ignore_type_in_groups=[(Enum, str)])
|
|
assert not diff3
|
|
|
|
def test_enum_use_enum_value1(self):
|
|
diff = DeepDiff("book", MyEnum2.book, use_enum_value=True)
|
|
assert not diff
|
|
|
|
def test_enum_use_enum_value_in_dict_key(self):
|
|
diff = DeepDiff({"book": 2}, {MyEnum2.book: 2}, use_enum_value=True)
|
|
assert not diff
|
|
|
|
def test_precompiled_regex(self):
|
|
|
|
pattern_1 = re.compile('foo')
|
|
pattern_2 = re.compile('foo')
|
|
pattern_3 = re.compile('foo', flags=re.I)
|
|
pattern_4 = re.compile('(foo)')
|
|
pattern_5 = re.compile('bar')
|
|
|
|
# same object
|
|
ddiff = DeepDiff(pattern_1, pattern_1)
|
|
result = {}
|
|
assert ddiff == result
|
|
|
|
# same pattern, different object
|
|
ddiff = DeepDiff(pattern_1, pattern_2)
|
|
result = {}
|
|
assert ddiff == result
|
|
|
|
# same pattern, different flags
|
|
ddiff = DeepDiff(pattern_1, pattern_3)
|
|
result = {
|
|
'values_changed': {
|
|
'root.flags': {
|
|
'new_value': 34,
|
|
'old_value': 32,
|
|
},
|
|
}
|
|
}
|
|
assert ddiff == result
|
|
|
|
# same pattern, different groups
|
|
ddiff = DeepDiff(pattern_1, pattern_4)
|
|
result = {
|
|
'values_changed': {
|
|
'root.pattern': {
|
|
'new_value': '(foo)',
|
|
'old_value': 'foo',
|
|
},
|
|
'root.groups': {
|
|
'new_value': 1,
|
|
'old_value': 0,
|
|
},
|
|
}
|
|
}
|
|
assert ddiff == result
|
|
|
|
# different pattern
|
|
ddiff = DeepDiff(pattern_1, pattern_5)
|
|
result = {
|
|
'values_changed': {
|
|
'root.pattern': {
|
|
'new_value': 'bar',
|
|
'old_value': 'foo',
|
|
},
|
|
}
|
|
}
|
|
assert ddiff == result
|
|
|
|
def test_custom_objects_change(self):
|
|
t1 = CustomClass(1)
|
|
t2 = CustomClass(2)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.a': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_slot_change(self):
|
|
class ClassA:
|
|
__slots__ = ('x', 'y')
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
t1 = ClassA(1, 1)
|
|
t2 = ClassA(1, 2)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.y': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_custom_class_changes_with_slot_changes(self):
|
|
class ClassA:
|
|
__slots__ = ['x', 'y']
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
class ClassB:
|
|
__slots__ = ['x']
|
|
|
|
ddiff = DeepDiff(ClassA, ClassB)
|
|
result = {'type_changes': {'root': {'old_type': ClassA, 'new_type': ClassB}}}
|
|
assert result == ddiff
|
|
|
|
def test_custom_class_changes_with_slot_change_when_ignore_type(self):
|
|
class ClassA:
|
|
__slots__ = ['x', 'y']
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
class ClassB:
|
|
__slots__ = ['x']
|
|
|
|
ddiff = DeepDiff(ClassA, ClassB, ignore_type_in_groups=[(ClassA, ClassB)])
|
|
result = {'attribute_removed': ['root.y']}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_slot_in_group_change(self):
|
|
class ClassA:
|
|
__slots__ = ('x', 'y')
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
class ClassB(ClassA):
|
|
pass
|
|
|
|
t1 = ClassA(1, 1)
|
|
t2 = ClassB(1, 1)
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(ClassA, ClassB)])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
|
|
def test_custom_class_changes_none_when_ignore_type(self):
|
|
ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)])
|
|
result = {
|
|
'type_changes': {
|
|
"root['a']": {
|
|
'old_type': type(None),
|
|
'new_type': int,
|
|
'old_value': None,
|
|
'new_value': 1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff1
|
|
|
|
ddiff2 = DeepDiff({'a': None}, {'a': '1'}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)])
|
|
assert {'values_changed': {"root['a']": {'new_value': '1', 'old_value': None}}} == ddiff2
|
|
|
|
ddiff3 = DeepDiff({'a': '1'}, {'a': None}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)])
|
|
assert {'values_changed': {"root['a']": {'new_value': None, 'old_value': '1'}}} == ddiff3
|
|
|
|
ddiff4 = DeepDiff({'a': {'b': None}}, {'a': {'b': '1'}}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)])
|
|
assert {'values_changed': {"root['a']['b']": {'new_value': '1', 'old_value': None}}} == ddiff4
|
|
|
|
ddiff5 = DeepDiff({'a': {'b': '1'}}, {'a': {'b': None}}, ignore_type_subclasses=True, ignore_type_in_groups=[(str, int, float, None)])
|
|
assert {'values_changed': {"root['a']['b']": {'new_value': None, 'old_value': '1'}}} == ddiff5
|
|
|
|
def test_custom_object_changes_when_ignore_type_in_groups(self):
|
|
class ClassA:
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
class ClassB:
|
|
def __init__(self, x):
|
|
self.x = x
|
|
|
|
class ClassC(ClassB):
|
|
def __repr__(self):
|
|
return "obj c"
|
|
|
|
obj_a = ClassA(1, 2)
|
|
obj_c = ClassC(3)
|
|
ddiff = DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=True)
|
|
result = {'type_changes': {'root': {'old_type': ClassA, 'new_type': ClassC, 'old_value': obj_a, 'new_value': obj_c}}}
|
|
assert result == ddiff
|
|
|
|
ddiff = DeepDiff(obj_a, obj_c, ignore_type_in_groups=[(ClassA, ClassB)], ignore_type_subclasses=False)
|
|
result = {'values_changed': {'root.x': {'new_value': 3, 'old_value': 1}}, 'attribute_removed': ['root.y']}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_slot_in_parent_class_change(self):
|
|
class ClassA:
|
|
__slots__ = ['x']
|
|
|
|
class ClassB(ClassA):
|
|
__slots__ = ['y']
|
|
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
t1 = ClassB(1, 1)
|
|
t2 = ClassB(2, 1)
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.x': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_with_single_protected_slot(self):
|
|
class ClassA:
|
|
__slots__ = '__a'
|
|
|
|
def __init__(self):
|
|
self.__a = 1
|
|
|
|
def __str__(self):
|
|
return str(self.__a)
|
|
|
|
t1 = ClassA()
|
|
t2 = ClassA()
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert {} == ddiff
|
|
|
|
def test_custom_objects_with_weakref_in_slots(self):
|
|
class ClassA:
|
|
__slots__ = ['a', '__weakref__']
|
|
|
|
def __init__(self, a):
|
|
self.a = a
|
|
|
|
t1 = ClassA(1)
|
|
t2 = ClassA(2)
|
|
diff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.a': {
|
|
'new_value': 2,
|
|
'old_value': 1
|
|
}
|
|
},
|
|
}
|
|
assert result == diff
|
|
|
|
def get_custom_objects_add_and_remove(self):
|
|
class ClassA:
|
|
a = 1
|
|
|
|
def __init__(self, b):
|
|
self.b = b
|
|
self.d = 10
|
|
|
|
t1 = ClassA(1)
|
|
t2 = ClassA(2)
|
|
t2.c = "new attribute"
|
|
del t2.d
|
|
return t1, t2
|
|
|
|
def test_custom_objects_add_and_remove(self):
|
|
t1, t2 = self.get_custom_objects_add_and_remove()
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'attribute_added': {'root.c'},
|
|
'values_changed': {
|
|
'root.b': {
|
|
'new_value': 2,
|
|
'old_value': 1
|
|
}
|
|
},
|
|
'attribute_removed': {'root.d'}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_add_and_remove_verbose(self):
|
|
t1, t2 = self.get_custom_objects_add_and_remove()
|
|
ddiff = DeepDiff(t1, t2, verbose_level=2)
|
|
result = {
|
|
'attribute_added': {
|
|
'root.c': 'new attribute'
|
|
},
|
|
'attribute_removed': {
|
|
'root.d': 10
|
|
},
|
|
'values_changed': {
|
|
'root.b': {
|
|
'new_value': 2,
|
|
'old_value': 1
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def get_custom_object_with_added_removed_methods(self):
|
|
class ClassA:
|
|
VAL = 1
|
|
VAL2 = 2
|
|
|
|
def method_a(self):
|
|
pass
|
|
|
|
t1 = ClassA()
|
|
t2 = ClassA()
|
|
|
|
def method_b(self):
|
|
pass
|
|
|
|
def method_c(self):
|
|
return "hello"
|
|
|
|
t2.method_b = method_b
|
|
t2.method_a = method_c
|
|
# Note that we are comparing ClassA instances. method_a originally was in ClassA
|
|
# But we also added another version of it to t2. So it comes up as
|
|
# added attribute.
|
|
return t1, t2
|
|
|
|
def test_custom_objects_add_and_remove_method(self):
|
|
t1, t2 = self.get_custom_object_with_added_removed_methods()
|
|
ddiff = DeepDiff(t1, t2)
|
|
|
|
result = {'attribute_added': {'root.method_a', 'root.method_b'}}
|
|
assert result == ddiff
|
|
|
|
def test_custom_objects_add_and_remove_method_verbose(self):
|
|
t1, t2 = self.get_custom_object_with_added_removed_methods()
|
|
ddiff = DeepDiff(t1, t2, verbose_level=2)
|
|
assert 'root.method_a' in ddiff['attribute_added']
|
|
assert 'root.method_b' in ddiff['attribute_added']
|
|
|
|
def test_set_of_custom_objects(self):
|
|
member1 = CustomClass(13, 37)
|
|
member2 = CustomClass(13, 37)
|
|
t1 = {member1}
|
|
t2 = {member2}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_dictionary_of_custom_objects(self):
|
|
member1 = CustomClass(13, 37)
|
|
member2 = CustomClass(13, 37)
|
|
t1 = {1: member1}
|
|
t2 = {1: member2}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_dictionary_with_string_keys1(self):
|
|
t1 = {"veggie": "carrots"}
|
|
t2 = {"meat": "carrots"}
|
|
|
|
diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
|
|
assert {'dictionary_item_added': ["root['meat']"],
|
|
'dictionary_item_removed': ["root['veggie']"]} == diff
|
|
|
|
def test_dictionary_with_string_keys_threshold_to_diff_deeper(self):
|
|
t1 = {"veggie": "carrots"}
|
|
t2 = {"meat": "carrots"}
|
|
|
|
diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0.33)
|
|
assert {'values_changed': {'root': {'new_value': {'meat': 'carrots'}, 'old_value': {'veggie': 'carrots'}}}} == diff
|
|
|
|
def test_dictionary_with_numeric_keys(self):
|
|
t1 = {Decimal('10.01'): "carrots"}
|
|
t2 = {10.01: "carrots"}
|
|
diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
|
|
assert {'dictionary_item_added': ["root[10.01]"], 'dictionary_item_removed': ["root[Decimal('10.01')]"]} == diff
|
|
|
|
diff2 = DeepDiff(t1, t2)
|
|
assert {'values_changed': {'root': {'new_value': {10.01: 'carrots'}, 'old_value': {Decimal('10.01'): 'carrots'}}}} == diff2
|
|
|
|
def test_loop(self):
|
|
class LoopTest:
|
|
def __init__(self, a):
|
|
self.loop = self
|
|
self.a = a
|
|
|
|
t1 = LoopTest(1)
|
|
t2 = LoopTest(2)
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.a': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_loop2(self):
|
|
class LoopTestA:
|
|
def __init__(self, a):
|
|
self.loop = LoopTestB
|
|
self.a = a
|
|
|
|
class LoopTestB:
|
|
def __init__(self, a):
|
|
self.loop = LoopTestA
|
|
self.a = a
|
|
|
|
t1 = LoopTestA(1)
|
|
t2 = LoopTestA(2)
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.a': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_loop3(self):
|
|
class LoopTest:
|
|
def __init__(self, a):
|
|
self.loop = LoopTest
|
|
self.a = a
|
|
|
|
t1 = LoopTest(1)
|
|
t2 = LoopTest(2)
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root.a': {
|
|
'old_value': 1,
|
|
'new_value': 2
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_loop_in_lists(self):
|
|
t1 = [1, 2, 3]
|
|
t1.append(t1)
|
|
|
|
t2 = [1, 2, 4]
|
|
t2.append(t2)
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[2]': {
|
|
'new_value': 4,
|
|
'old_value': 3
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_loop_in_lists2(self):
|
|
t1 = [1, 2, [3]]
|
|
t1[2].append(t1)
|
|
t2 = [1, 2, [4]]
|
|
t2[2].append(t2)
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[2][0]': {
|
|
'old_value': 3,
|
|
'new_value': 4
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_decimal(self):
|
|
t1 = {1: Decimal('10.1')}
|
|
t2 = {1: Decimal('2.2')}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[1]': {
|
|
'new_value': Decimal('2.2'),
|
|
'old_value': Decimal('10.1')
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_unicode_string_type_changes(self):
|
|
"""
|
|
These tests were written when DeepDiff was in Python 2.
|
|
Writing b"你好" throws an exception in Python 3 so can't be used for testing.
|
|
These tests are currently useless till they are rewritten properly.
|
|
"""
|
|
unicode_string = {"hello": "你好"}
|
|
ascii_string = {"hello": "你好"}
|
|
ddiff = DeepDiff(unicode_string, ascii_string)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_unicode_string_value_changes(self):
|
|
unicode_string = {"hello": "你好"}
|
|
ascii_string = {"hello": "你好hello"}
|
|
ddiff = DeepDiff(unicode_string, ascii_string)
|
|
result = {
|
|
'values_changed': {
|
|
"root['hello']": {
|
|
'old_value': '你好',
|
|
'new_value': '你好hello'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_unicode_string_value_and_type_changes(self):
|
|
unicode_string = {"hello": "你好"}
|
|
ascii_string = {"hello": "你好hello"}
|
|
ddiff = DeepDiff(unicode_string, ascii_string)
|
|
# In python3, all string is unicode, so these 2 strings only diff
|
|
# in values
|
|
result = {
|
|
'values_changed': {
|
|
"root['hello']": {
|
|
'new_value': '你好hello',
|
|
'old_value': '你好'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_int_to_unicode_string(self):
|
|
t1 = 1
|
|
ascii_string = "你好"
|
|
ddiff = DeepDiff(t1, ascii_string)
|
|
# In python3, all string is unicode, so these 2 strings only diff
|
|
# in values
|
|
result = {
|
|
'type_changes': {
|
|
'root': {
|
|
'old_type': int,
|
|
'new_type': str,
|
|
'old_value': 1,
|
|
'new_value': '你好'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_int_to_unicode(self):
|
|
t1 = 1
|
|
unicode_string = "你好"
|
|
ddiff = DeepDiff(t1, unicode_string)
|
|
# In python3, all string is unicode, so these 2 strings only diff
|
|
# in values
|
|
result = {
|
|
'type_changes': {
|
|
'root': {
|
|
'old_type': int,
|
|
'new_type': str,
|
|
'old_value': 1,
|
|
'new_value': '你好'
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
@pytest.mark.parametrize("test_num, t1, t2, ignore_numeric_type_changes, significant_digits, number_format_notation, result", [
|
|
(1, 43.265798602382986, 43.71677762295505, False, 0, "f", {'values_changed': {'root': {'new_value': 43.71677762295505, 'old_value': 43.265798602382986}}}), # Note that it rounds the number so one becomes 43 and the other one is 44
|
|
(2, Decimal('2.5'), Decimal('1.5'), False, 0, "f", {}),
|
|
(3, Decimal('2.5'), Decimal('1.5'), False, 1, "f", {'values_changed': {'root': {'new_value': Decimal('1.5'), 'old_value': Decimal('2.5')}}}),
|
|
(4, Decimal('2.5'), Decimal(2.5), False, 3, "f", {}),
|
|
(5, 1024, 1022, False, 2, "e", {}),
|
|
(6, {"key": [Decimal('2.0001'), Decimal('20000.0001')]}, {"key": [2.0002, 20000.0002]}, True, 4, "e", {'values_changed': {"root['key'][0]": {'new_value': 2.0002, 'old_value': Decimal('2.0001')}}}),
|
|
(7, [Decimal("999.99999999")], [Decimal("999.9999999999")], False, 6, "f", {}),
|
|
])
|
|
def test_significant_digits_and_notation(self, test_num, t1, t2, ignore_numeric_type_changes, significant_digits, number_format_notation, result):
|
|
ddiff = DeepDiff(t1, t2, significant_digits=significant_digits, number_format_notation=number_format_notation,
|
|
ignore_numeric_type_changes=ignore_numeric_type_changes)
|
|
assert result == ddiff, f"test_significant_digits_and_notation #{test_num} failed."
|
|
|
|
def test_significant_digits_for_complex_imaginary_part(self):
|
|
t1 = 1.23 + 1.222254j
|
|
t2 = 1.23 + 1.222256j
|
|
ddiff = DeepDiff(t1, t2, significant_digits=4)
|
|
assert {} == ddiff
|
|
result = {
|
|
'values_changed': {
|
|
'root': {
|
|
'new_value': (1.23 + 1.222256j),
|
|
'old_value': (1.23 + 1.222254j)
|
|
}
|
|
}
|
|
}
|
|
ddiff = DeepDiff(t1, t2, significant_digits=5)
|
|
assert result == ddiff
|
|
|
|
def test_significant_digits_for_complex_real_part(self):
|
|
t1 = 1.23446879 + 1.22225j
|
|
t2 = 1.23446764 + 1.22225j
|
|
ddiff = DeepDiff(t1, t2, significant_digits=5)
|
|
assert {} == ddiff
|
|
|
|
def test_significant_digits_for_list_of_floats(self):
|
|
t1 = [1.2344, 5.67881, 6.778879]
|
|
t2 = [1.2343, 5.67882, 6.778878]
|
|
ddiff = DeepDiff(t1, t2, significant_digits=3)
|
|
assert {} == ddiff
|
|
ddiff = DeepDiff(t1, t2, significant_digits=4)
|
|
result = {
|
|
'values_changed': {
|
|
'root[0]': {
|
|
'new_value': 1.2343,
|
|
'old_value': 1.2344
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t2, significant_digits=5)
|
|
result = {
|
|
'values_changed': {
|
|
'root[0]': {
|
|
'new_value': 1.2343,
|
|
'old_value': 1.2344
|
|
},
|
|
'root[1]': {
|
|
'new_value': 5.67882,
|
|
'old_value': 5.67881
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
ddiff = DeepDiff(t1, t2)
|
|
ddiff2 = DeepDiff(t1, t2, significant_digits=6)
|
|
assert ddiff2 == ddiff
|
|
|
|
def test_negative_significant_digits(self):
|
|
with pytest.raises(ValueError):
|
|
DeepDiff(1, 1, significant_digits=-1)
|
|
|
|
@pytest.mark.parametrize("t1, t2, significant_digits, expected_result",
|
|
[
|
|
(10, 10.0, 5, {}),
|
|
(10, 10.2, 5, {'values_changed': {'root': {'new_value': 10.2, 'old_value': 10}}}),
|
|
(10, 10.2, 0, {}),
|
|
(Decimal(10), 10, 0, {}),
|
|
(Decimal(10), 10, 10, {}),
|
|
(Decimal(10), 10.0, 0, {}),
|
|
(Decimal(10), 10.0, 10, {}),
|
|
(Decimal('10.0'), 10.0, 5, {}),
|
|
(Decimal('10.01'), 10.01, 1, {}),
|
|
(Decimal('10.01'), 10.01, 2, {}),
|
|
(Decimal('10.01'), 10.01, 5, {}),
|
|
(Decimal('10.01'), 10.01, 8, {}),
|
|
(Decimal('10.010'), 10.01, 3, {}),
|
|
(Decimal('100000.1'), 100000.1, 0, {}),
|
|
(Decimal('100000.1'), 100000.1, 1, {}),
|
|
(Decimal('100000.1'), 100000.1, 5, {}),
|
|
(Decimal('100000'), 100000.1, 0, {}),
|
|
(Decimal('100000'), 100000.1, 1, {'values_changed': {'root': {'new_value': 100000.1, 'old_value': Decimal('100000')}}}),
|
|
(np_float64(123.93420232), 123.93420232, 0, {}),
|
|
])
|
|
def test_decimal_digits(self, t1, t2, significant_digits, expected_result):
|
|
ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, ignore_string_type_changes=True, significant_digits=significant_digits)
|
|
assert expected_result == ddiff
|
|
|
|
@pytest.mark.parametrize('test_num, t1, t2, log_scale_similarity_threshold, expected', [
|
|
(
|
|
1,
|
|
{'foo': 110, 'bar': 306}, # t1
|
|
{'foo': 140, 'bar': 298}, # t2
|
|
0.01, # threshold
|
|
{'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}, "root['bar']": {'new_value': 298, 'old_value': 306}}}, # expected
|
|
),
|
|
(
|
|
2,
|
|
{'foo': 110, 'bar': 306}, # t1
|
|
{'foo': 140, 'bar': 298}, # t2
|
|
0.1, # threshold
|
|
{'values_changed': {"root['foo']": {'new_value': 140, 'old_value': 110}}}, # expected
|
|
),
|
|
(
|
|
2,
|
|
{'foo': 110, 'bar': 306}, # t1
|
|
{'foo': 140, 'bar': 298}, # t2
|
|
0.3, # threshold
|
|
{}, # expected
|
|
),
|
|
])
|
|
def test_log_scale(self, test_num, t1, t2, log_scale_similarity_threshold, expected):
|
|
diff = DeepDiff(t1, t2, use_log_scale=True, log_scale_similarity_threshold=log_scale_similarity_threshold)
|
|
assert expected == diff, f"test_log_scale #{test_num} failed."
|
|
|
|
def test_ignore_type_in_groups(self):
|
|
t1 = [1, 2, 3]
|
|
t2 = [1.0, 2.0, 3.0]
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers)
|
|
assert not ddiff
|
|
|
|
def test_ignore_type_in_groups2(self):
|
|
t1 = [1, 2, 3]
|
|
t2 = [1.0, 2.0, 3.3]
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers)
|
|
result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}}
|
|
assert result == ddiff
|
|
|
|
def test_ignore_type_in_groups3(self):
|
|
t1 = {Decimal('10.01'): "carrots"}
|
|
t2 = {10.01: "carrots"}
|
|
|
|
diff1 = DeepDiff(t1, t2, threshold_to_diff_deeper=0)
|
|
|
|
diff2 = DeepDiff(t1, t2, ignore_numeric_type_changes=True)
|
|
|
|
diff3 = DeepDiff(t1, t2, ignore_type_in_groups=DeepDiff.numbers)
|
|
assert {'dictionary_item_added': ["root[10.01]"], 'dictionary_item_removed': ["root[Decimal('10.01')]"]} == diff1
|
|
assert {} == diff2 == diff3
|
|
|
|
def test_ignore_type_in_groups_just_numbers(self):
|
|
t1 = [1, 2, 3, 'a']
|
|
t2 = [1.0, 2.0, 3.3, b'a']
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[DeepDiff.numbers])
|
|
result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}},
|
|
'type_changes': {'root[3]': {'new_type': bytes,
|
|
'new_value': b'a',
|
|
'old_type': str,
|
|
'old_value': 'a'}}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_ignore_type_in_groups_numbers_and_strings(self):
|
|
t1 = [1, 2, 3, 'a']
|
|
t2 = [1.0, 2.0, 3.3, b'a']
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[DeepDiff.numbers, DeepDiff.strings])
|
|
result = {'values_changed': {'root[2]': {'new_value': 3.3, 'old_value': 3}}}
|
|
assert result == ddiff
|
|
|
|
def test_ignore_type_in_groups_none_and_objects(self):
|
|
t1 = [1, 2, 3, 'a', None]
|
|
t2 = [1.0, 2.0, 3.3, b'a', 'hello']
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(1, 1.0), (None, str, bytes)])
|
|
result = {
|
|
'values_changed': {
|
|
'root[2]': {
|
|
'new_value': 3.3,
|
|
'old_value': 3
|
|
},
|
|
'root[4]': {
|
|
'new_value': 'hello',
|
|
'old_value': None
|
|
}
|
|
}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_ignore_type_in_groups_str_and_datetime(self):
|
|
now = datetime.datetime(2022, 4, 10, 0, 40, 41, 357857)
|
|
t1 = [1, 2, 3, 'a', now]
|
|
t2 = [1, 2, 3, 'a', 'now']
|
|
ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(str, bytes, datetime.datetime)])
|
|
now_utc = now.replace(tzinfo=datetime.timezone.utc)
|
|
result = {'values_changed': {'root[4]': {'new_value': 'now', 'old_value': now_utc}}}
|
|
assert result == ddiff
|
|
|
|
def test_ignore_type_in_groups_float_vs_decimal(self):
|
|
diff = DeepDiff(float('0.1'), Decimal('0.1'), ignore_type_in_groups=[(float, Decimal)], significant_digits=2)
|
|
assert not diff
|
|
|
|
@pytest.mark.parametrize("t1, t2, significant_digits, result", [
|
|
([0.1], [Decimal('0.10')], 55,
|
|
{'values_changed': {'root[0]': {'new_value': Decimal('0.10'), 'old_value': 0.1}}}), # Due to floating point arithmetics with high significant digits.
|
|
([0.1], [Decimal('0.10')], 5, {}), # Same inputs as above but with significant digits that is low.
|
|
([-0.1], [-Decimal('0.10')], 5, {}),
|
|
([-Decimal('0.102')], [-Decimal('0.10')], 2, {}),
|
|
([1], [Decimal('1.00000002')], 3, {}),
|
|
])
|
|
def test_ignore_numeric_type_changes_numbers_when_decimal(self, t1, t2, significant_digits, result):
|
|
ddiff = DeepDiff(t1, t2, ignore_numeric_type_changes=True, significant_digits=significant_digits)
|
|
assert result == ddiff
|
|
|
|
@pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE")
|
|
def test_base_level_dictionary_remapping(self):
|
|
"""
|
|
Since subclassed dictionaries that override __getitem__ treat newdict.get(key)
|
|
differently than newdict['key'], we are unable to create a unittest with
|
|
self.assertIn() and have to resort to fetching the values of two keys and making
|
|
sure they are the same value.
|
|
"""
|
|
t1 = {1: 1, 2: 2}
|
|
t2 = {2: 2, 3: 3}
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert ddiff['dic_item_added'] == ddiff['dictionary_item_added']
|
|
assert ddiff['dic_item_removed'] == ddiff['dictionary_item_removed']
|
|
|
|
@pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE")
|
|
def test_index_and_repeat_dictionary_remapping(self):
|
|
t1 = [1, 3, 1, 4]
|
|
t2 = [4, 4, 1]
|
|
ddiff = DeepDiff(t1, t2, ignore_order=True, report_repetition=True)
|
|
assert ddiff['repetition_change']['root[0]']['newindexes'] == ddiff['repetition_change']['root[0]']['new_indexes']
|
|
assert ddiff['repetition_change']['root[0]']['newrepeat'] == ddiff['repetition_change']['root[0]']['new_repeat']
|
|
assert ddiff['repetition_change']['root[0]']['oldindexes'] == ddiff['repetition_change']['root[0]']['old_indexes']
|
|
assert ddiff['repetition_change']['root[0]']['oldrepeat'] == ddiff['repetition_change']['root[0]']['old_repeat']
|
|
|
|
@pytest.mark.skip(reason="REMAPPING DISABLED UNTIL KEY NAMES CHANGE AGAIN IN FUTURE")
|
|
def test_value_and_type_dictionary_remapping(self):
|
|
t1 = {1: 1, 2: 2}
|
|
t2 = {1: 1, 2: '2'}
|
|
ddiff = DeepDiff(t1, t2)
|
|
assert ddiff['type_changes']['root[2]']['newtype'] == ddiff['type_changes']['root[2]']['new_type']
|
|
assert ddiff['type_changes']['root[2]']['newvalue'] == ddiff['type_changes']['root[2]']['new_value']
|
|
assert ddiff['type_changes']['root[2]']['oldtype'] == ddiff['type_changes']['root[2]']['old_type']
|
|
assert ddiff['type_changes']['root[2]']['oldvalue'] == ddiff['type_changes']['root[2]']['old_value']
|
|
|
|
def test_skip_type(self):
|
|
l1 = logging.getLogger("test")
|
|
l2 = logging.getLogger("test2")
|
|
t1 = {"log": l1, 2: 1337}
|
|
t2 = {"log": l2, 2: 1337}
|
|
ddiff = DeepDiff(t1, t2, exclude_types={logging.Logger})
|
|
assert {} == ddiff
|
|
|
|
t1 = {"log": "book", 2: 1337}
|
|
t2 = {"log": l2, 2: 1337}
|
|
ddiff = DeepDiff(t1, t2, exclude_types={logging.Logger})
|
|
assert {} == ddiff
|
|
|
|
def test_skip_path1(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["veggies", "tofu", "soy sauce"]
|
|
}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"})
|
|
assert {} == ddiff
|
|
|
|
def test_skip_path2(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan"}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"})
|
|
assert {} == ddiff
|
|
|
|
def test_skip_path2_key_names(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan"}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths={"ingredients"})
|
|
assert {} == ddiff
|
|
|
|
def test_skip_path2_reverse(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan"}
|
|
ddiff = DeepDiff(t2, t1, exclude_paths={"root['ingredients']"})
|
|
assert {} == ddiff
|
|
|
|
def test_exclude_path_when_prefix_of_exclude_path_matches1(self):
|
|
diff = DeepDiff({}, {'foo': '', 'bar': ''}, exclude_paths=['foo', 'bar'])
|
|
assert not diff
|
|
|
|
def test_include_path3(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan"}
|
|
ddiff = DeepDiff(t2, t1, include_paths={"root['for_life']"})
|
|
assert {} == ddiff
|
|
|
|
def test_include_path3_with_just_key_names(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan"}
|
|
ddiff = DeepDiff(t1, t2, include_paths={"for_life"})
|
|
assert {} == ddiff
|
|
|
|
def test_include_path4_nested(self):
|
|
t1 = {
|
|
"foo": {"bar": "potato"},
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {
|
|
"foo": {"bar": "banana"},
|
|
"ingredients": ["bread", "cheese"]
|
|
}
|
|
ddiff = DeepDiff(t1, t2, include_paths="foo")
|
|
assert {
|
|
'values_changed': {
|
|
"root['foo']['bar']": {
|
|
'new_value': 'banana',
|
|
'old_value': 'potato'
|
|
}
|
|
}
|
|
} == ddiff
|
|
|
|
def test_include_path5(self):
|
|
diff = DeepDiff(
|
|
{
|
|
'name': 'Testname',
|
|
'code': 'bla',
|
|
'noneCode': 'blu',
|
|
}, {
|
|
'uid': '12345',
|
|
'name': 'Testname2',
|
|
},
|
|
)
|
|
|
|
diff2 = DeepDiff(
|
|
{
|
|
'name': 'Testname',
|
|
'code': 'bla',
|
|
'noneCode': 'blu',
|
|
}, {
|
|
'uid': '12345',
|
|
'name': 'Testname2',
|
|
},
|
|
include_paths = "root['name']"
|
|
)
|
|
expected = {'values_changed': {'root': {'new_value': {'uid': '12345', 'name': 'Testname2'}, 'old_value': {'name': 'Testname', 'code': 'bla', 'noneCode': 'blu'}}}}
|
|
expected2 = {'values_changed': {"root['name']": {'new_value': 'Testname2', 'old_value': 'Testname'}}}
|
|
|
|
assert expected == diff
|
|
assert expected2 == diff2
|
|
|
|
def test_include_path6(self):
|
|
t1 = [1, 2, 3, [4, 5, {6: 7}]]
|
|
t2 = [1, 2, 3, [4, 5, {6: 1000}]]
|
|
diff = DeepDiff(
|
|
t1,
|
|
t2,
|
|
)
|
|
|
|
diff2 = DeepDiff(
|
|
t1,
|
|
t2,
|
|
include_paths = "root[3]"
|
|
)
|
|
|
|
diff3 = DeepDiff(
|
|
t1,
|
|
t2,
|
|
include_paths = "root[4]"
|
|
)
|
|
expected = {'values_changed': {'root[3][2][6]': {'new_value': 1000, 'old_value': 7}}}
|
|
assert expected == diff
|
|
assert diff == diff2
|
|
assert not diff3
|
|
|
|
def test_skip_path4(self):
|
|
t1 = {
|
|
"for life": "vegan",
|
|
"ingredients": ["no meat", "no eggs", "no dairy"]
|
|
}
|
|
t2 = {"for life": "vegan", "zutaten": ["veggies", "tofu", "soy sauce"]}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths={"root['ingredients']"})
|
|
assert 'dictionary_item_added' in ddiff, {}
|
|
assert 'dictionary_item_removed' not in ddiff, {}
|
|
|
|
def test_skip_path5(self):
|
|
t1 = [{'cn': 'tuser', 'first_name': 'Test', 'last_name': 'User', 'name': 'Test User', 'email': 'tuser@example.com'}]
|
|
t2 = [{'name': 'Test User', 'email': 'tuser@example.com'}]
|
|
|
|
diff = DeepDiff(
|
|
t1,
|
|
t2,
|
|
ignore_order=True,
|
|
exclude_paths={
|
|
"root[0]['cn']",
|
|
"root[0]['first_name']",
|
|
"root[0]['last_name']"
|
|
})
|
|
|
|
assert not diff
|
|
|
|
def test_skip_custom_object_path(self):
|
|
t1 = CustomClass(1)
|
|
t2 = CustomClass(2)
|
|
ddiff = DeepDiff(t1, t2, exclude_paths=['root.a'])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_list_path(self):
|
|
t1 = ['a', 'b']
|
|
t2 = ['a']
|
|
ddiff = DeepDiff(t1, t2, exclude_paths=['root[1]'])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_dictionary_path(self):
|
|
t1 = {1: {2: "a"}}
|
|
t2 = {1: {}}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths=['root[1][2]'])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_dictionary_path_with_custom_object(self):
|
|
obj1 = CustomClass(1)
|
|
obj2 = CustomClass(2)
|
|
|
|
t1 = {1: {2: obj1}}
|
|
t2 = {1: {2: obj2}}
|
|
ddiff = DeepDiff(t1, t2, exclude_paths=['root[1][2].a'])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
# TODO: fix it for python 3.5, 3.6 and pypy3
|
|
def test_skip_regexp(self):
|
|
t1 = [{'a': 1, 'b': 2}, {'c': 4, 'b': 5}]
|
|
t2 = [{'a': 1, 'b': 3}, {'c': 4, 'b': 5}]
|
|
ddiff = DeepDiff(t1, t2, exclude_regex_paths=[r"root\[\d+\]\['b'\]"])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_regexp_with_default_threshold_to_diff_deeper(self):
|
|
t1 = {'a': [1, 2, [3, {'foo1': 'bar'}]]}
|
|
t2 = {'a': [1, 2, [3, {'foo2': 'bar'}]]}
|
|
exclude_regex_paths = [r"\['foo.'\]"]
|
|
equivalent_exclude_paths = ["root['a'][2][1]['foo1']", "root['a'][2][1]['foo2']"]
|
|
|
|
# The excluded keys must not count towards threshold_to_diff_deeper, otherwise the
|
|
# whole dictionary is reported as a single change and the excluded keys leak into
|
|
# old_value and new_value.
|
|
assert {} == DeepDiff(t1, t2, exclude_regex_paths=exclude_regex_paths)
|
|
assert DeepDiff(t1, t2, exclude_paths=equivalent_exclude_paths) == DeepDiff(
|
|
t1, t2, exclude_regex_paths=exclude_regex_paths)
|
|
assert DeepDiff(t1, t2, exclude_regex_paths=exclude_regex_paths, threshold_to_diff_deeper=0) == DeepDiff(
|
|
t1, t2, exclude_regex_paths=exclude_regex_paths)
|
|
|
|
def test_skip_regexp_with_default_threshold_to_diff_deeper_reports_remaining_keys(self):
|
|
t1 = {'foo1': 'bar', 'foo2': 'x', 'foo3': 'y', 'keep': 1}
|
|
t2 = {'foo4': 'bar', 'foo5': 'x', 'foo6': 'y', 'keep': 2}
|
|
|
|
# Only 'keep' is shared, so the excluded keys alone would drag the ratio of shared
|
|
# keys under threshold_to_diff_deeper and hide the change on 'keep'.
|
|
ddiff = DeepDiff(t1, t2, exclude_regex_paths=[r"root\['foo.'\]"])
|
|
result = {'values_changed': {"root['keep']": {'new_value': 2, 'old_value': 1}}}
|
|
assert result == ddiff
|
|
|
|
def test_include_obj_callback(self):
|
|
def include_obj_callback(obj, path):
|
|
return True if "include" in path or isinstance(obj, int) else False
|
|
|
|
t1 = {"x": 10, "y": "b", "z": "c", "include_me": "a"}
|
|
t2 = {"x": 10, "y": "c", "z": "b", "include_me": "b"}
|
|
ddiff = DeepDiff(t1, t2, include_obj_callback=include_obj_callback)
|
|
result = {'values_changed': {"root['include_me']": {'new_value': "b", 'old_value': "a"}}}
|
|
assert result == ddiff
|
|
assert {"root['include_me']"} == ddiff.affected_paths
|
|
assert {"include_me"} == ddiff.affected_root_keys
|
|
|
|
def test_include_obj_callback_strict(self):
|
|
def include_obj_callback_strict(obj, path):
|
|
return True if isinstance(obj, int) and obj > 10 else False
|
|
|
|
t1 = {"x": 11, "y": 10, "z": "c"}
|
|
t2 = {"x": 12, "y": 12, "z": "c"}
|
|
ddiff = DeepDiff(t1, t2, include_obj_callback_strict=include_obj_callback_strict)
|
|
result = {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 11}}}
|
|
assert result == ddiff
|
|
assert {"root['x']"} == ddiff.affected_paths
|
|
assert {"x"} == ddiff.affected_root_keys
|
|
|
|
def test_skip_exclude_obj_callback(self):
|
|
def exclude_obj_callback(obj, path):
|
|
return True if "skip" in path or isinstance(obj, int) else False
|
|
|
|
t1 = {"x": 10, "y": "b", "z": "c", "skip_1": 0}
|
|
t2 = {"x": 12, "y": "b", "z": "c", "skip_2": 0}
|
|
ddiff = DeepDiff(t1, t2, exclude_obj_callback=exclude_obj_callback)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_exclude_obj_callback_strict(self):
|
|
def exclude_obj_callback_strict(obj, path):
|
|
return True if isinstance(obj, int) and obj > 10 else False
|
|
|
|
t1 = {"x": 10, "y": "b", "z": "c"}
|
|
t2 = {"x": 12, "y": "b", "z": "c"}
|
|
ddiff = DeepDiff(t1, t2, exclude_obj_callback_strict=exclude_obj_callback_strict)
|
|
result = {'values_changed': {"root['x']": {'new_value': 12, 'old_value': 10}}}
|
|
assert result == ddiff
|
|
assert {"root['x']"} == ddiff.affected_paths
|
|
assert {"x"} == ddiff.affected_root_keys
|
|
|
|
def test_skip_str_type_in_dictionary(self):
|
|
t1 = {1: {2: "a"}}
|
|
t2 = {1: {}}
|
|
ddiff = DeepDiff(t1, t2, exclude_types=[str])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_skip_str_type_in_dict_on_list(self):
|
|
t1 = [{1: "a"}]
|
|
t2 = [{}]
|
|
ddiff = DeepDiff(t1, t2, exclude_types=[str])
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_unknown_parameters(self):
|
|
with pytest.raises(ValueError):
|
|
DeepDiff(1, 1, wrong_param=2)
|
|
|
|
def test_bad_attribute(self):
|
|
class Bad:
|
|
__slots__ = ['x', 'y']
|
|
|
|
def __getattr__(self, key):
|
|
raise AttributeError("Bad item")
|
|
|
|
def __str__(self):
|
|
return "Bad Object"
|
|
|
|
t1 = Bad()
|
|
t2 = Bad()
|
|
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {}
|
|
assert result == ddiff
|
|
|
|
def test_dict_none_item_removed(self):
|
|
t1 = {1: None, 2: 2}
|
|
t2 = {2: 2}
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'dictionary_item_removed': {'root[1]'}
|
|
}
|
|
assert result == ddiff
|
|
|
|
def test_list_none_item_removed(self):
|
|
t1 = [1, 2, None]
|
|
t2 = [1, 2]
|
|
ddiff = DeepDiff(t1, t2)
|
|
result = {
|
|
'iterable_item_removed': {'root[2]': None}
|
|
}
|
|
assert result == ddiff
|
|
assert {"root[2]"} == ddiff.affected_paths
|
|
|
|
def test_list_item_removed_from_the_middle(self):
|
|
t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c']
|
|
t2 = [0, 1, 2, 3, 5, 6, 7, 8, 'a', 'b', 'c']
|
|
diff = DeepDiff(t1, t2)
|
|
result = {'iterable_item_removed': {'root[4]': 'bye'}}
|
|
assert result == diff
|
|
assert {"root[4]"} == diff.affected_paths
|
|
assert {4} == diff.affected_root_keys
|
|
|
|
# TODO: we need to support reporting that items have been swapped
|
|
# def test_item_moved(self):
|
|
# # currently all the items in the list need to be hashables
|
|
# t1 = [1, 2, 3, 4]
|
|
# t2 = [4, 2, 3, 1]
|
|
# diff = DeepDiff(t1, t2)
|
|
# result = {} # it should show that those items are swapped.
|
|
# assert result == diff
|
|
|
|
def test_list_item_values_replace_in_the_middle(self):
|
|
t1 = [0, 1, 2, 3, 'bye', 5, 6, 7, 8, 'a', 'b', 'c']
|
|
t2 = [0, 1, 2, 3, 'see', 'you', 'later', 5, 6, 7, 8, 'a', 'b', 'c']
|
|
diff = DeepDiff(t1, t2)
|
|
result = {
|
|
'values_changed': {
|
|
'root[4]': {
|
|
'old_value': 'bye',
|
|
'new_value': 'see',
|
|
}
|
|
},
|
|
'iterable_item_added': {
|
|
'root[5]': 'you',
|
|
'root[6]': 'later'
|
|
}
|
|
}
|
|
assert result == diff
|
|
assert {'root[5]', 'root[6]', 'root[4]'} == diff.affected_paths
|
|
assert {4, 5, 6} == diff.affected_root_keys
|
|
|
|
def test_non_subscriptable_iterable(self):
|
|
def gen1():
|
|
yield 42
|
|
yield 1337
|
|
yield 31337
|
|
|
|
def gen2():
|
|
yield 42
|
|
yield 1337
|
|
|
|
t1 = gen1()
|
|
t2 = gen2()
|
|
ddiff = DeepDiff(t1, t2)
|
|
|
|
result = {'iterable_item_removed': {'root[2]': 31337}}
|
|
# Note: In text-style results, we currently pretend this stuff is subscriptable for readability
|
|
|
|
assert result == ddiff
|
|
assert {"root[2]"} == ddiff.affected_paths
|
|
|
|
@pytest.mark.parametrize('t1, t2, params, expected_result', [
|
|
(float('nan'), float('nan'), {}, ['values_changed']),
|
|
(float('nan'), float('nan'), {'ignore_nan_inequality': True}, []),
|
|
([1, float('nan')], [1, float('nan')], {'ignore_nan_inequality': True}, []),
|
|
])
|
|
@pytest.mark.skipif(pypy3, reason="some versions of pypy3 have nan==nan")
|
|
def test_ignore_nan_inequality(self, t1, t2, params, expected_result):
|
|
assert expected_result == list(DeepDiff(t1, t2, **params).keys())
|
|
|
|
@pytest.mark.parametrize('ignore_order, ignore_private_variables, expected', [
|
|
(True, True, {}),
|
|
(False, True, {}),
|
|
(True, False, {'values_changed': {"root[0]['schema']['items']['__ref']": {'new_value': 2, 'old_value': 1}}}),
|
|
(False, False, {'values_changed': {"root[0]['schema']['items']['__ref']": {'new_value': 2, 'old_value': 1}}}),
|
|
])
|
|
def test_private_variables(self, ignore_order, ignore_private_variables, expected):
|
|
t1 = [{'in': 'body', 'name': 'params', 'description': 'params', 'required': True, 'schema': {'type': 'array', 'items': {'__ref': 1}}}]
|
|
t2 = [{'in': 'body', 'name': 'params', 'description': 'params', 'required': True, 'schema': {'type': 'array', 'items': {'__ref': 2}}}]
|
|
diff = DeepDiff(t1, t2, ignore_order=ignore_order, ignore_private_variables=ignore_private_variables)
|
|
assert expected == diff
|
|
|
|
def test_group_by1(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Brown'},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {"root[1]['last_name']": {
|
|
'new_value': 'Brown',
|
|
'old_value': 'Blue'}}}
|
|
assert expected == diff
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id')
|
|
expected_grouped = {'values_changed': {"root['BB']['last_name']": {
|
|
'new_value': 'Brown',
|
|
'old_value': 'Blue'}}}
|
|
assert expected_grouped == diff
|
|
|
|
def test_group_by2_when_repeats(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20},
|
|
{'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
|
|
]
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id', group_by_sort_key='name')
|
|
expected_grouped = {
|
|
'values_changed': {
|
|
"root['BB'][0]['last_name']": {
|
|
'new_value': 'Brown',
|
|
'old_value': 'Blue'
|
|
}
|
|
},
|
|
'iterable_item_removed': {
|
|
"root['BB'][1]": {
|
|
'name': 'Jimmy',
|
|
'last_name': 'Red',
|
|
'int_id': 3
|
|
}
|
|
}
|
|
}
|
|
assert expected_grouped == diff
|
|
|
|
diff2 = DeepDiff(t1, t2, group_by='id', group_by_sort_key=lambda x: x['name'])
|
|
assert expected_grouped == diff2
|
|
|
|
def test_group_by3_when_repeats_and_group_by_list(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Blue', 'int_id': 20},
|
|
{'id': 'BB', 'name': 'Jimmy', 'last_name': 'Red', 'int_id': 3},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody', 'int_id': 2},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Brown', 'int_id': 20},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple', 'int_id': 4},
|
|
]
|
|
|
|
diff1 = DeepDiff(t1, t2, group_by=['id', 'name'])
|
|
expected_grouped = {
|
|
'dictionary_item_removed': ["root['BB']['Jimmy']"],
|
|
'values_changed': {
|
|
"root['BB']['James']['last_name']": {
|
|
'new_value': 'Brown',
|
|
'old_value': 'Blue'
|
|
}
|
|
}
|
|
}
|
|
assert expected_grouped == diff1
|
|
|
|
diff2 = DeepDiff(t1, t2, group_by=['id', 'name'], group_by_sort_key='int_id')
|
|
expected_grouped = {
|
|
'dictionary_item_removed': ["root['BB']['Jimmy']"],
|
|
'values_changed': {
|
|
"root['BB']['James'][0]['last_name']": {
|
|
'new_value': 'Brown',
|
|
'old_value': 'Blue'
|
|
}
|
|
}
|
|
}
|
|
assert expected_grouped == diff2
|
|
|
|
def test_group_by_key_missing(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
|
|
{'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB', 'name': 'James', 'last_name': 'Blue'},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id')
|
|
expected = {'dictionary_item_added': ["root[2]['id']"]}
|
|
assert expected == diff
|
|
|
|
def test_group_by_not_dict1(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
None,
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB'},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id')
|
|
expected = {
|
|
'type_changes': {
|
|
'root[1]': {
|
|
'old_type': None.__class__,
|
|
'new_type': dict,
|
|
'old_value': None,
|
|
'new_value': {
|
|
'id': 'BB'
|
|
}
|
|
}
|
|
},
|
|
}
|
|
assert expected == diff
|
|
|
|
def test_group_by_not_dict2(self):
|
|
t1 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
{'id': 'BB'},
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
t2 = [
|
|
{'id': 'AA', 'name': 'Joe', 'last_name': 'Nobody'},
|
|
None,
|
|
{'id': 'CC', 'name': 'Mike', 'last_name': 'Apple'},
|
|
]
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id')
|
|
expected = {
|
|
'type_changes': {
|
|
'root[1]': {
|
|
'old_type': dict,
|
|
'new_type': None.__class__,
|
|
'new_value': None,
|
|
'old_value': {
|
|
'id': 'BB'
|
|
}
|
|
}
|
|
},
|
|
}
|
|
assert expected == diff
|
|
|
|
def test_group_by_not_list_of_dicts(self):
|
|
t1 = {1: 2}
|
|
|
|
t2 = {1: 3}
|
|
|
|
diff = DeepDiff(t1, t2, group_by='id')
|
|
expected = {'values_changed': {'root[1]': {'new_value': 3, 'old_value': 2}}}
|
|
assert expected == diff
|
|
assert {"root[1]"} == diff.affected_paths
|
|
|
|
def test_datetime_in_key(self):
|
|
|
|
now = datetime.datetime(2022, 4, 10, 0, 40, 41, 357857)
|
|
t1 = {now: 1, now + datetime.timedelta(1): 4}
|
|
t2 = {now: 2, now + datetime.timedelta(1): 4}
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {f'root[{repr(now)}]': {'new_value': 2, 'old_value': 1}}}
|
|
|
|
assert expected == diff
|
|
|
|
def test_property_values(self):
|
|
|
|
class A:
|
|
_thing = 0
|
|
|
|
def __init__(self, a):
|
|
self.a = a
|
|
|
|
@property
|
|
def thing(self):
|
|
A._thing += 1
|
|
return A._thing
|
|
|
|
@property
|
|
def __thing2(self):
|
|
A._thing += 1
|
|
return A._thing
|
|
|
|
diff = DeepDiff(A(1), A(1))
|
|
expected = {
|
|
'values_changed': {
|
|
'root._thing': {
|
|
'new_value': 1,
|
|
'old_value': 0
|
|
},
|
|
'root.thing': {
|
|
'new_value': 2,
|
|
'old_value': 1
|
|
}
|
|
}
|
|
}
|
|
|
|
assert expected == diff
|
|
|
|
diff2 = DeepDiff(A(1), A(1), ignore_private_variables=False)
|
|
expected2 = {
|
|
'values_changed': {
|
|
'root._A__thing2': {
|
|
'new_value': 5,
|
|
'old_value': 3
|
|
},
|
|
'root._thing': {
|
|
'new_value': 5,
|
|
'old_value': 3
|
|
},
|
|
'root.thing': {
|
|
'new_value': 6,
|
|
'old_value': 4
|
|
}
|
|
}
|
|
}
|
|
|
|
assert expected2 == diff2
|
|
|
|
def test_diffs_rrules(self):
|
|
|
|
from dateutil.rrule import MONTHLY, rrule
|
|
|
|
d = DeepDiff(
|
|
rrule(freq=MONTHLY, count=5, dtstart=datetime.datetime(2014, 12, 31)),
|
|
rrule(freq=MONTHLY, count=4, dtstart=datetime.datetime(2011, 12, 31)),
|
|
)
|
|
|
|
assert d == {
|
|
"values_changed": {
|
|
"root[0]": {
|
|
"new_value": datetime.datetime(2011, 12, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
"old_value": datetime.datetime(2014, 12, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
},
|
|
"root[1]": {
|
|
"new_value": datetime.datetime(2012, 1, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
"old_value": datetime.datetime(2015, 1, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
},
|
|
"root[2]": {
|
|
"new_value": datetime.datetime(2012, 3, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
"old_value": datetime.datetime(2015, 3, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
},
|
|
"root[3]": {
|
|
"new_value": datetime.datetime(2012, 5, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
"old_value": datetime.datetime(2015, 5, 31, 0, 0, tzinfo=datetime.timezone.utc),
|
|
},
|
|
},
|
|
"iterable_item_removed": {"root[4]": datetime.datetime(2015, 7, 31, 0, 0)},
|
|
}
|
|
|
|
def test_pydantic1(self):
|
|
|
|
class Foo(PydanticBaseModel):
|
|
thing: int = None
|
|
that: str
|
|
|
|
t1 = Foo(thing=1, that='yes')
|
|
t2 = Foo(thing=2, that='yes')
|
|
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {'root.thing': {'new_value': 2, 'old_value': 1}}}
|
|
assert expected == diff
|
|
|
|
def test_pydantic2(self):
|
|
|
|
class Foo(PydanticBaseModel):
|
|
thing: int = None
|
|
that: str
|
|
|
|
class Bar(PydanticBaseModel):
|
|
stuff: List[Foo]
|
|
|
|
t1 = Bar(stuff=[Foo(thing=1, that='yes')])
|
|
t2 = Bar(stuff=[Foo(thing=2, that='yes')])
|
|
|
|
diff = DeepDiff(t1, t2)
|
|
expected = {'values_changed': {'root.stuff[0].thing': {'new_value': 2, 'old_value': 1}}}
|
|
assert expected == diff
|
|
|
|
def test_dataclass1(self):
|
|
|
|
|
|
t1 = MyDataClass(1, 4)
|
|
t2 = MyDataClass(2, 4)
|
|
|
|
diff = DeepDiff(t1, t2, exclude_regex_paths=["any"])
|
|
assert {'values_changed': {'root.val': {'new_value': 2, 'old_value': 1}}} == diff
|
|
|
|
def test_dataclass2(self):
|
|
|
|
@dataclass(frozen=True)
|
|
class MyDataClass:
|
|
val: int
|
|
val2: int
|
|
|
|
t1 = {
|
|
MyDataClass(1, 4): 10,
|
|
MyDataClass(2, 4): 20,
|
|
}
|
|
|
|
t2 = {
|
|
MyDataClass(1, 4): 10,
|
|
MyDataClass(2, 4): 10,
|
|
}
|
|
|
|
diff = DeepDiff(t1, t2, exclude_regex_paths=["any"])
|
|
assert {'values_changed': {'root[MyDataClass(val=2,val2=4)]': {'new_value': 10, 'old_value': 20}}} == diff
|
|
|
|
|
|
def test_group_by_with_none_key_and_ignore_case(self):
|
|
"""Test that group_by works with None keys when ignore_string_case is True"""
|
|
dict1 = [{'txt_field': 'FULL_NONE', 'group_id': None}, {'txt_field': 'FULL', 'group_id': 'a'}]
|
|
dict2 = [{'txt_field': 'PARTIAL_NONE', 'group_id': None}, {'txt_field': 'PARTIAL', 'group_id': 'a'}]
|
|
|
|
diff = DeepDiff(
|
|
dict1,
|
|
dict2,
|
|
ignore_order=True,
|
|
group_by='group_id',
|
|
ignore_string_case=True
|
|
)
|
|
|
|
expected = {'values_changed': {"root[None]['txt_field']":
|
|
{'new_value': 'partial_none', 'old_value': 'full_none'},
|
|
"root['a']['txt_field']":
|
|
{'new_value': 'partial', 'old_value': 'full'}
|
|
}
|
|
}
|
|
assert expected == diff
|
|
|
|
def test_affected_root_keys_when_dict_empty(self):
|
|
diff = DeepDiff({}, {1:1, 2:2}, threshold_to_diff_deeper=0)
|
|
assert [1, 2] == diff.affected_root_keys
|
|
|
|
diff2 = DeepDiff({}, {1:1, 2:2})
|
|
assert [] == diff2.affected_root_keys
|
|
|
|
def test_range1(self):
|
|
range1 = range(0, 10)
|
|
range2 = range(0, 8)
|
|
diff = DeepDiff(range1, range2)
|
|
assert {'iterable_item_removed': {'root[8]': 8, 'root[9]': 9}} == diff
|
|
|
|
|
|
def test_group_by_that_has_integers(self):
|
|
"""Test that group_by with integer keys doesn't add type prefixes like 'int:33'"""
|
|
t1 = [{'row_num_in_file': 33, 'value': 'old'}]
|
|
t2 = [{'row_num_in_file': 33, 'value': 'new'}]
|
|
|
|
diff = DeepDiff(t1, t2, group_by='row_num_in_file', ignore_string_type_changes=True)
|
|
|
|
# Verify that the diff key contains the integer 33 without type prefix
|
|
changes = diff.get('values_changed', {})
|
|
assert len(changes) == 1
|
|
key = list(changes.keys())[0]
|
|
assert "int:" not in key
|
|
assert "[33]" in key or "['33']" in key
|