Files
seperman-deepdiff/tests/test_operators.py
T

429 lines
15 KiB
Python

import re
import math
import pytest
from copy import deepcopy
from typing import List, Any
from deepdiff import DeepDiff
from deepdiff.operator import BaseOperator, PrefixOrSuffixOperator, BaseOperatorPlus
class TestOperators:
def test_custom_operators_prevent_default(self):
t1 = {
"coordinates": [
{"x": 5, "y": 5},
{"x": 8, "y": 8}
]
}
t2 = {
"coordinates": [
{"x": 6, "y": 6},
{"x": 88, "y": 88}
]
}
class L2DistanceDifferWithPreventDefault(BaseOperator):
def __init__(self, regex_paths: List[str], distance_threshold: float):
super().__init__(regex_paths)
self.distance_threshold = distance_threshold
def _l2_distance(self, c1, c2):
return math.sqrt(
(c1["x"] - c2["x"]) ** 2 + (c1["y"] - c2["y"]) ** 2
)
def give_up_diffing(self, level, diff_instance) -> bool:
l2_distance = self._l2_distance(level.t1, level.t2)
if l2_distance > self.distance_threshold:
diff_instance.custom_report_result('distance_too_far', level, {
"l2_distance": l2_distance
})
return True
ddiff = DeepDiff(t1, t2, custom_operators=[L2DistanceDifferWithPreventDefault(
["^root\\['coordinates'\\]\\[\\d+\\]$"],
1
)])
expected = {
'distance_too_far': {
"root['coordinates'][0]": {'l2_distance': 1.4142135623730951},
"root['coordinates'][1]": {'l2_distance': 113.13708498984761}
}
}
assert expected == ddiff
def test_custom_operators_not_prevent_default(self):
t1 = {
"coordinates": [
{"x": 5, "y": 5},
{"x": 8, "y": 8}
]
}
t2 = {
"coordinates": [
{"x": 6, "y": 6},
{"x": 88, "y": 88}
]
}
class L2DistanceDifferWithPreventDefault(BaseOperator):
def __init__(self, regex_paths, distance_threshold):
super().__init__(regex_paths)
self.distance_threshold = distance_threshold
def _l2_distance(self, c1, c2):
return math.sqrt(
(c1["x"] - c2["x"]) ** 2 + (c1["y"] - c2["y"]) ** 2
)
def give_up_diffing(self, level, diff_instance) -> bool:
l2_distance = self._l2_distance(level.t1, level.t2)
if l2_distance > self.distance_threshold:
diff_instance.custom_report_result('distance_too_far', level, {
"l2_distance": l2_distance
})
#
return False
ddiff = DeepDiff(t1, t2, custom_operators=[L2DistanceDifferWithPreventDefault(
["^root\\['coordinates'\\]\\[\\d+\\]$"],
1
)
])
expected = {
'values_changed': {
"root['coordinates'][0]['x']": {'new_value': 6, 'old_value': 5},
"root['coordinates'][0]['y']": {'new_value': 6, 'old_value': 5},
"root['coordinates'][1]['x']": {'new_value': 88, 'old_value': 8},
"root['coordinates'][1]['y']": {'new_value': 88, 'old_value': 8}
},
'distance_too_far': {
"root['coordinates'][0]": {'l2_distance': 1.4142135623730951},
"root['coordinates'][1]": {'l2_distance': 113.13708498984761}
}
}
assert expected == ddiff
def test_custom_operators_should_not_equal(self):
t1 = {
"id": 5,
"expect_change_pos": 10,
"expect_change_neg": 10,
}
t2 = {
"id": 5,
"expect_change_pos": 100,
"expect_change_neg": 10,
}
class ExpectChangeOperator(BaseOperator):
def __init__(self, regex_paths):
super().__init__(regex_paths)
def give_up_diffing(self, level, diff_instance) -> bool:
if level.t1 == level.t2:
diff_instance.custom_report_result('unexpected:still', level, {
"old": level.t1,
"new": level.t2
})
return True
ddiff = DeepDiff(t1, t2, custom_operators=[
ExpectChangeOperator(regex_paths=["root\\['expect_change.*'\\]"])
])
assert ddiff == {'unexpected:still': {"root['expect_change_neg']": {'old': 10, 'new': 10}}}
def test_custom_operator2(self):
class CustomClass:
def __init__(self, d: dict, l: list):
self.dict = d
self.dict['list'] = l
def __repr__(self):
return "Class list is " + str(self.dict['list'])
custom1 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3])
custom2 = CustomClass(d=dict(c=3, d=4), l=[1, 2, 3, 2])
custom3 = CustomClass(d=dict(a=1, b=2), l=[1, 2, 3, 4])
class ListMatchOperator(BaseOperator):
def give_up_diffing(self, level, diff_instance) -> bool:
if set(level.t1.dict['list']) == set(level.t2.dict['list']):
return True
return False
ddiff = DeepDiff(custom1, custom2, custom_operators=[
ListMatchOperator(types=[CustomClass])
])
assert {} == ddiff
ddiff2 = DeepDiff(custom2, custom3, threshold_to_diff_deeper=0, custom_operators=[
ListMatchOperator(types=[CustomClass])
])
expected = {
'dictionary_item_added': ["root.dict['a']", "root.dict['b']"],
'dictionary_item_removed': ["root.dict['c']", "root.dict['d']"],
'values_changed': {"root.dict['list'][3]": {'new_value': 4, 'old_value': 2}}}
assert expected == ddiff2
def test_include_only_certain_path(self):
class MyOperator:
def __init__(self, include_paths):
self.include_paths = include_paths
def match(self, level) -> bool:
return True
def give_up_diffing(self, level, diff_instance) -> bool:
return level.path() not in self.include_paths
t1 = {'a': [10, 11], 'b': [20, 21], 'c': [30, 31]}
t2 = {'a': [10, 22], 'b': [20, 33], 'c': [30, 44]}
ddiff = DeepDiff(t1, t2, custom_operators=[
MyOperator(include_paths="root['a'][1]")
])
expected = {'values_changed': {"root['a'][1]": {'new_value': 22, 'old_value': 11}}}
assert expected == ddiff
def test_give_up_diffing_on_first_diff(self):
class MyOperator:
def match(self, level) -> bool:
return True
def give_up_diffing(self, level, diff_instance) -> bool:
return any(diff_instance.tree.values())
t1 = [[1, 2], [3, 4], [5, 6]]
t2 = [[1, 3], [3, 5], [5, 7]]
ddiff = DeepDiff(t1, t2, custom_operators=[
MyOperator()
])
expected = {'values_changed': {'root[0][1]': {'new_value': 3, 'old_value': 2}}}
assert expected == ddiff
def test_prefix_or_suffix_diff(self):
t1 = {
"key1": ["foo", "bar's food", "jack", "joe"]
}
t2 = {
"key1": ["foo", "bar", "jill", "joe'car"]
}
ddiff = DeepDiff(t1, t2, custom_operators=[
PrefixOrSuffixOperator()
])
expected = {'values_changed': {"root['key1'][2]": {'new_value': 'jill', 'old_value': 'jack'}}}
assert expected == ddiff
with pytest.raises(NotImplementedError) as exp:
DeepDiff(t1, t2, ignore_order=True, custom_operators=[
PrefixOrSuffixOperator()
])
expected2 = 'PrefixOrSuffixOperator needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.'
assert expected2 == str(exp.value)
def test_custom_operator3_small_numbers(self):
x = [2.0000000000000027, 2.500000000000005, 2.000000000000002, 3.000000000000001]
y = [2.000000000000003, 2.500000000000005, 2.0000000000000027, 3.0000000000000027]
result = DeepDiff(x, y)
expected = {
'values_changed': {
'root[0]': {'new_value': 2.000000000000003, 'old_value': 2.0000000000000027},
'root[2]': {'new_value': 2.0000000000000027, 'old_value': 2.000000000000002},
'root[3]': {'new_value': 3.0000000000000027, 'old_value': 3.000000000000001}}}
assert expected == result
class CustomCompare(BaseOperatorPlus):
def __init__(self, tolerance, types):
self.tolerance = tolerance
self.types = types
def match(self, level) -> bool:
if type(level.t1) in self.types:
return True
return False
def give_up_diffing(self, level, diff_instance) -> bool:
relative = abs(abs(level.t1 - level.t2) / level.t1)
if not max(relative, self.tolerance) == self.tolerance:
custom_report = f'relative diff: {relative:.8e}'
diff_instance.custom_report_result('diff', level, custom_report)
return True
def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any:
return obj
def compare_func(x, y, level):
return True
operators = [CustomCompare(types=[float], tolerance=5.5e-5)]
result2 = DeepDiff(x, y, custom_operators=operators, iterable_compare_func=compare_func)
assert {} == result2
result3 = DeepDiff(x, y, custom_operators=operators, zip_ordered_iterables=True)
assert {} == result3, "We should get the same result as result2 when zip_ordered_iterables is True."
def test_custom_operator_and_ignore_order1_using_base_operator_plus(self):
d1 = {
"Name": "SUB_OBJECT_FILES",
"Values": {
"Value": [
"{f254498b-b752-4f35-bef5-6f1844b61eb7}",
"{7fb2a550-1849-45c0-b273-9aa5e4eb9f2b}",
"{3a614c62-4252-48eb-b279-1450ee8af182}",
"{208f22c4-c256-4311-9a45-e6c37d343458}",
"{1fcf5d37-ef19-43a7-a1ad-d17c7c1713c6}",
]
}
}
d2 = {
"Name": "SUB_OBJECT_FILES",
"Values": {
"Value": [
"{e5d18917-1a2c-4abe-b601-8ec002629953}",
"{ea71ba1f-1339-4fae-bc28-a9ce9b8a8c67}",
"{66bb6192-9cd2-4074-8be1-f2ac52877c70}",
"{0c88b900-3755-4d10-93ef-b6a96dbcba90}",
"{e39fdfc5-be6c-4f97-9345-9a8286381fe7}"
]
}
}
class RemoveGUIDsOperator(BaseOperatorPlus):
_pattern = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
_substitute = "guid"
def match(self, level) -> bool:
return isinstance(level.t1, str) and isinstance(level.t2, str)
@classmethod
def _remove_pattern(cls, t: str):
return re.sub(cls._pattern, cls._substitute, t)
def give_up_diffing(self, level, diff_instance):
t1 = self._remove_pattern(level.t1)
t2 = self._remove_pattern(level.t2)
return t1 == t2
def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any:
"""
Used for ignore_order=True
"""
if isinstance(obj, str):
return self._remove_pattern(obj)
return obj
operator = RemoveGUIDsOperator()
diff1 = DeepDiff(d1, d2, custom_operators=[operator], log_stacktrace=True)
assert not diff1
diff2 = DeepDiff(d1, d2, ignore_order=True, custom_operators=[operator], log_stacktrace=True)
assert not diff2
def test_custom_operator_and_ignore_order2(self):
d1 = {
"Entity": {
"Property": {
"Name": "SUB_OBJECT_FILES",
"Values": {
"Value": [
"{f254498b-b752-4f35-bef5-6f1844b61eb7}",
"{7fb2a550-1849-45c0-b273-9aa5e4eb9f2b}",
"{3a614c62-4252-48eb-b279-1450ee8af182}",
"{208f22c4-c256-4311-9a45-e6c37d343458}",
"{1fcf5d37-ef19-43a7-a1ad-d17c7c1713c6}",
"{a9cbecc0-21dc-49ce-8b2c-d36352dae139}"
]
}
}
}
}
d2 = {
"Entity": {
"Property": {
"Name": "SUB_OBJECT_FILES",
"Values": {
"Value": [
"{e5d18917-1a2c-4abe-b601-8ec002629953}",
"{ea71ba1f-1339-4fae-bc28-a9ce9b8a8c67}",
"{d7778018-a7b5-4246-8caa-f590138d99e5}",
"{66bb6192-9cd2-4074-8be1-f2ac52877c70}",
"{0c88b900-3755-4d10-93ef-b6a96dbcba90}",
"{e39fdfc5-be6c-4f97-9345-9a8286381fe7}"
]
}
}
}
}
class RemovePatternOperator(BaseOperator):
_pattern: str = ""
_substitute: str = ""
@classmethod
def _remove_pattern(cls, t: str):
return re.sub(cls._pattern, cls._substitute, t)
def give_up_diffing(self, level, diff_instance):
if isinstance(level.t1, str) and isinstance(level.t2, str):
t1 = self._remove_pattern(level.t1)
t2 = self._remove_pattern(level.t2)
return t1 == t2
return False
class RemoveGUIDsOperator(RemovePatternOperator):
_pattern = r"[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"
_substitute = "guid"
diff1 = DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=False, custom_operators=[RemoveGUIDsOperator(types=[str])])
assert not diff1
with pytest.raises(NotImplementedError) as exp:
DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=True, custom_operators=[RemoveGUIDsOperator(types=[str])])
expected2 = 'RemoveGUIDsOperator needs to define a normalize_value_for_hashing method to be compatible with ignore_order=True or iterable_compare_func.'
assert expected2 == str(exp.value)
# --------- Let's implement the normalize_value_for_hashing to make it work with ignore_order=True ---------
class RemoveGUIDsOperatorIgnoreOrderReady(RemoveGUIDsOperator):
def normalize_value_for_hashing(self, parent: Any, obj: Any) -> Any:
if isinstance(obj, str):
return self._remove_pattern(obj)
return obj
diff3 = DeepDiff(deepcopy(d1), deepcopy(d2), ignore_order=True, custom_operators=[RemoveGUIDsOperatorIgnoreOrderReady(types=[str])])
assert not diff3, "We shouldn't have a diff because we have normalized the string values to be all the same vlues."