Skip to content

Commit d7bff4a

Browse files
authored
Merge pull request #528 from seperman/dev
8.4.2
2 parents 185eacb + 61cdaf6 commit d7bff4a

11 files changed

+148
-40
lines changed

deepdiff/base.py

+2-11
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,12 @@
1-
from typing import Protocol, Any
1+
from typing import Any
22
from deepdiff.helper import strings, numbers, SetOrdered
33

44

55
DEFAULT_SIGNIFICANT_DIGITS_WHEN_IGNORE_NUMERIC_TYPES = 12
66
TYPE_STABILIZATION_MSG = 'Unable to stabilize the Numpy array {} due to {}. Please set ignore_order=False.'
77

88

9-
class BaseProtocol(Protocol):
10-
t1: Any
11-
t2: Any
12-
cutoff_distance_for_pairs: float
13-
use_log_scale: bool
14-
log_scale_similarity_threshold: float
15-
view: str
16-
17-
18-
class Base(BaseProtocol):
9+
class Base:
1910
numbers = numbers
2011
strings = strings
2112

deepdiff/deephash.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from enum import Enum
1010
from deepdiff.helper import (strings, numbers, times, unprocessed, not_hashed, add_to_frozen_set,
11-
convert_item_or_items_into_set_else_none, get_doc,
11+
convert_item_or_items_into_set_else_none, get_doc, ipranges,
1212
convert_item_or_items_into_compiled_regexes_else_none,
1313
get_id, type_is_subclass_of_type_group, type_in_type_group,
1414
number_to_string, datetime_normalize, KEY_TO_VAL_STR,
@@ -142,7 +142,7 @@ class DeepHash(Base):
142142
__doc__ = doc
143143

144144
def __init__(self,
145-
obj,
145+
obj: Any,
146146
*,
147147
apply_hash=True,
148148
custom_operators: Optional[List[Any]] =None,
@@ -484,6 +484,11 @@ def _prep_number(self, obj):
484484
number_format_notation=self.number_format_notation)
485485
return KEY_TO_VAL_STR.format(type_, obj)
486486

487+
def _prep_ipranges(self, obj):
488+
type_ = 'iprange'
489+
obj = str(obj)
490+
return KEY_TO_VAL_STR.format(type_, obj)
491+
487492
def _prep_datetime(self, obj):
488493
type_ = 'datetime'
489494
obj = datetime_normalize(self.truncate_datetime, obj, default_timezone=self.default_timezone)
@@ -558,6 +563,9 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
558563
elif isinstance(obj, numbers): # type: ignore
559564
result = self._prep_number(obj)
560565

566+
elif isinstance(obj, ipranges):
567+
result = self._prep_ipranges(obj)
568+
561569
elif isinstance(obj, MutableMapping):
562570
result, counts = self._prep_dict(obj=obj, parent=parent, parents_ids=parents_ids)
563571

deepdiff/diff.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from enum import Enum
1313
from copy import deepcopy
1414
from math import isclose as is_close
15-
from typing import List, Dict, Callable, Union, Any, Pattern, Tuple, Optional, Set, FrozenSet, TYPE_CHECKING
15+
from typing import List, Dict, Callable, Union, Any, Pattern, Tuple, Optional, Set, FrozenSet, TYPE_CHECKING, Protocol
1616
from collections.abc import Mapping, Iterable, Sequence
1717
from collections import defaultdict
1818
from inspect import getmembers
@@ -27,7 +27,7 @@
2727
np_ndarray, np_floating, get_numpy_ndarray_rows, RepeatedTimer,
2828
TEXT_VIEW, TREE_VIEW, DELTA_VIEW, detailed__dict__, add_root_to_paths,
2929
np, get_truncate_datetime, dict_, CannotCompare, ENUM_INCLUDE_KEYS,
30-
PydanticBaseModel, Opcode, SetOrdered)
30+
PydanticBaseModel, Opcode, SetOrdered, ipranges)
3131
from deepdiff.serialization import SerializationMixin
3232
from deepdiff.distance import DistanceMixin, logarithmic_similarity
3333
from deepdiff.model import (
@@ -119,7 +119,17 @@ def _report_progress(_stats, progress_logger, duration):
119119
)
120120

121121

122-
class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, Base):
122+
class DeepDiffProtocol(Protocol):
123+
t1: Any
124+
t2: Any
125+
cutoff_distance_for_pairs: float
126+
use_log_scale: bool
127+
log_scale_similarity_threshold: float
128+
view: str
129+
130+
131+
132+
class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, DeepDiffProtocol, Base):
123133
__doc__ = doc
124134

125135
CACHE_AUTO_ADJUST_THRESHOLD = 0.25
@@ -1501,6 +1511,11 @@ def _diff_numbers(self, level, local_tree=None, report_type_change=True):
15011511
if t1_s != t2_s:
15021512
self._report_result('values_changed', level, local_tree=local_tree)
15031513

1514+
def _diff_ipranges(self, level, local_tree=None):
1515+
"""Diff IP ranges"""
1516+
if str(level.t1) != str(level.t2):
1517+
self._report_result('values_changed', level, local_tree=local_tree)
1518+
15041519
def _diff_datetime(self, level, local_tree=None):
15051520
"""Diff DateTimes"""
15061521
level.t1 = datetime_normalize(self.truncate_datetime, level.t1, default_timezone=self.default_timezone)
@@ -1695,6 +1710,9 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=
16951710
elif isinstance(level.t1, datetime.datetime):
16961711
self._diff_datetime(level, local_tree=local_tree)
16971712

1713+
elif isinstance(level.t1, ipranges):
1714+
self._diff_ipranges(level, local_tree=local_tree)
1715+
16981716
elif isinstance(level.t1, (datetime.date, datetime.timedelta, datetime.time)):
16991717
self._diff_time(level, local_tree=local_tree)
17001718

deepdiff/distance.py

+34-12
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,45 @@
11
import math
22
import datetime
3-
from deepdiff.base import BaseProtocol
3+
from typing import TYPE_CHECKING, Callable, Protocol, Any
44
from deepdiff.deephash import DeepHash
55
from deepdiff.helper import (
66
DELTA_VIEW, numbers, strings, add_to_frozen_set, not_found, only_numbers, np, np_float64, time_to_seconds,
77
cartesian_product_numpy, np_ndarray, np_array_factory, get_homogeneous_numpy_compatible_type_of_seq, dict_,
88
CannotCompare)
99
from collections.abc import Mapping, Iterable
1010

11+
if TYPE_CHECKING:
12+
from deepdiff.diff import DeepDiffProtocol
1113

12-
DISTANCE_CALCS_NEEDS_CACHE = "Distance calculation can not happen once the cache is purged. Try with _cache='keep'"
14+
class DistanceProtocol(DeepDiffProtocol, Protocol):
15+
hashes: dict
16+
deephash_parameters: dict
17+
iterable_compare_func: Callable | None
18+
math_epsilon: float
19+
cutoff_distance_for_pairs: float
20+
21+
def __get_item_rough_length(self, item, parent:str="root") -> float:
22+
...
1323

24+
def _to_delta_dict(
25+
self,
26+
directed: bool = True,
27+
report_repetition_required: bool = True,
28+
always_include_values: bool = False,
29+
) -> dict:
30+
...
1431

32+
def __calculate_item_deephash(self, item: Any) -> None:
33+
...
1534

1635

17-
class DistanceMixin(BaseProtocol):
1836

19-
def _get_rough_distance(self):
37+
DISTANCE_CALCS_NEEDS_CACHE = "Distance calculation can not happen once the cache is purged. Try with _cache='keep'"
38+
39+
40+
class DistanceMixin:
41+
42+
def _get_rough_distance(self: "DistanceProtocol"):
2043
"""
2144
Gives a numeric value for the distance of t1 and t2 based on how many operations are needed to convert
2245
one to the other.
@@ -51,7 +74,7 @@ def _get_rough_distance(self):
5174

5275
return diff_length / (t1_len + t2_len)
5376

54-
def __get_item_rough_length(self, item, parent='root'):
77+
def __get_item_rough_length(self: "DistanceProtocol", item, parent='root'):
5578
"""
5679
Get the rough length of an item.
5780
It is used as a part of calculating the rough distance between objects.
@@ -69,7 +92,7 @@ def __get_item_rough_length(self, item, parent='root'):
6992
length = DeepHash.get_key(self.hashes, key=item, default=None, extract_index=1)
7093
return length
7194

72-
def __calculate_item_deephash(self, item):
95+
def __calculate_item_deephash(self: "DistanceProtocol", item: Any) -> None:
7396
DeepHash(
7497
item,
7598
hashes=self.hashes,
@@ -79,8 +102,7 @@ def __calculate_item_deephash(self, item):
79102
)
80103

81104
def _precalculate_distance_by_custom_compare_func(
82-
self, hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
83-
105+
self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
84106
pre_calced_distances = dict_()
85107
for added_hash in hashes_added:
86108
for removed_hash in hashes_removed:
@@ -99,7 +121,7 @@ def _precalculate_distance_by_custom_compare_func(
99121
return pre_calced_distances
100122

101123
def _precalculate_numpy_arrays_distance(
102-
self, hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
124+
self: "DistanceProtocol", hashes_added, hashes_removed, t1_hashtable, t2_hashtable, _original_type):
103125

104126
# We only want to deal with 1D arrays.
105127
if isinstance(t2_hashtable[next(iter(hashes_added))].item, (np_ndarray, list)):
@@ -203,7 +225,7 @@ def _get_numbers_distance(num1, num2, max_=1, use_log_scale=False, log_scale_sim
203225
return 0
204226
if use_log_scale:
205227
distance = logarithmic_distance(num1, num2)
206-
if distance < logarithmic_distance:
228+
if distance < 0:
207229
return 0
208230
return distance
209231
if not isinstance(num1, float):
@@ -246,7 +268,7 @@ def numpy_apply_log_keep_sign(array, offset=MATH_LOG_OFFSET):
246268
return signed_log_values
247269

248270

249-
def logarithmic_similarity(a: numbers, b: numbers, threshold: float=0.1):
271+
def logarithmic_similarity(a: numbers, b: numbers, threshold: float=0.1) -> float:
250272
"""
251273
A threshold of 0.1 translates to about 10.5% difference.
252274
A threshold of 0.5 translates to about 65% difference.
@@ -255,7 +277,7 @@ def logarithmic_similarity(a: numbers, b: numbers, threshold: float=0.1):
255277
return logarithmic_distance(a, b) < threshold
256278

257279

258-
def logarithmic_distance(a: numbers, b: numbers):
280+
def logarithmic_distance(a: numbers, b: numbers) -> float:
259281
# Apply logarithm to the absolute values and consider the sign
260282
a = float(a)
261283
b = float(b)

deepdiff/helper.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import string
99
import time
1010
import enum
11-
from typing import NamedTuple, Any, List, Optional, Dict, Union, TYPE_CHECKING
11+
import ipaddress
12+
from typing import NamedTuple, Any, List, Optional, Dict, Union, TYPE_CHECKING, Tuple
1213
from ast import literal_eval
1314
from decimal import Decimal, localcontext, InvalidOperation as InvalidDecimalOperation
1415
from itertools import repeat
@@ -184,9 +185,10 @@ def get_semvar_as_integer(version):
184185
only_complex_number = (complex,) + numpy_complex_numbers
185186
only_numbers = (int, float, complex, Decimal) + numpy_numbers
186187
datetimes = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time)
188+
ipranges = (ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network)
187189
uuids = (uuid.UUID, )
188190
times = (datetime.datetime, datetime.time)
189-
numbers = only_numbers + datetimes
191+
numbers: Tuple = only_numbers + datetimes
190192
booleans = (bool, np_bool_)
191193

192194
basic_types = strings + numbers + uuids + booleans + (type(None), )

deepdiff/search.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66

77
from deepdiff.helper import (
8-
strings, numbers, add_to_frozen_set, get_doc, dict_, RE_COMPILED_TYPE
8+
strings, numbers, add_to_frozen_set, get_doc, dict_, RE_COMPILED_TYPE, ipranges
99
)
1010

1111
logger = logging.getLogger(__name__)
@@ -115,7 +115,7 @@ def __init__(self,
115115
matched_values=self.__set_or_dict(),
116116
unprocessed=[])
117117
self.use_regexp = use_regexp
118-
if not strict_checking and isinstance(item, numbers):
118+
if not strict_checking and (isinstance(item, numbers) or isinstance(item, ipranges)):
119119
item = str(item)
120120
if self.use_regexp:
121121
try:
@@ -312,6 +312,9 @@ def __search(self, obj, item, parent="root", parents_ids=frozenset()):
312312
elif isinstance(obj, strings) and isinstance(item, numbers):
313313
return
314314

315+
elif isinstance(obj, ipranges):
316+
self.__search_str(str(obj), item, parent)
317+
315318
elif isinstance(obj, numbers):
316319
self.__search_numbers(obj, item, parent)
317320

deepdiff/serialization.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
pydantic_base_model_type,
3333
PydanticBaseModel,
3434
NotPresent,
35+
ipranges,
3536
)
3637
from deepdiff.model import DeltaResult
3738

@@ -112,7 +113,8 @@ class UnsupportedFormatErr(TypeError):
112113
'SetOrdered': SetOrdered,
113114
'namedtuple': collections.namedtuple,
114115
'OrderedDict': collections.OrderedDict,
115-
'Pattern': re.Pattern,
116+
'Pattern': re.Pattern,
117+
'iprange': str,
116118
}
117119

118120

deepdiff/summarize.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ def calculate_weights(node):
2222
weight = 0
2323
children_weights = {}
2424
for k, v in node.items():
25-
edge_weight = len(k)
25+
try:
26+
edge_weight = len(k)
27+
except TypeError:
28+
edge_weight = 1
2629
child_weight, child_structure = calculate_weights(v)
2730
total_weight = edge_weight + child_weight
2831
weight += total_weight
@@ -133,6 +136,9 @@ def greedy_tree_summarization_balanced(json_data: JSON, max_weight: int, balance
133136

134137

135138
def summarize(data: JSON, max_length:int=200, balance_threshold:float=0.6) -> str:
136-
return json_dumps(
137-
greedy_tree_summarization_balanced(data, max_length, balance_threshold)
138-
)
139+
try:
140+
return json_dumps(
141+
greedy_tree_summarization_balanced(data, max_length, balance_threshold)
142+
)
143+
except Exception:
144+
return str(data)

tests/test_diff_text.py

+6
Original file line numberDiff line numberDiff line change
@@ -2252,3 +2252,9 @@ def test_affected_root_keys_when_dict_empty(self):
22522252

22532253
diff2 = DeepDiff({}, {1:1, 2:2})
22542254
assert [] == diff2.affected_root_keys
2255+
2256+
def test_range1(self):
2257+
range1 = range(0, 10)
2258+
range2 = range(0, 8)
2259+
diff = DeepDiff(range1, range2)
2260+
assert {'iterable_item_removed': {'root[8]': 8, 'root[9]': 9}} == diff

tests/test_hash.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
import pytz
55
import logging
66
import datetime
7+
import ipaddress
8+
from typing import Union
79
from pathlib import Path
810
from collections import namedtuple
911
from functools import partial
1012
from enum import Enum
11-
from deepdiff import DeepHash
13+
from deepdiff import DeepDiff, DeepHash
1214
from deepdiff.deephash import (
1315
prepare_string_for_hashing, unprocessed,
1416
UNPROCESSED_KEY, BoolObj, HASH_LOOKUP_ERR_MSG, combine_hashes_lists)
@@ -999,10 +1001,39 @@ def test_combine_hashes_lists(self, items, prefix, expected):
9991001
(7, b"First have a cup of potatos. Then \xc3\x28 cup of flour", None, False, UnicodeDecodeError, EXPECTED_MESSAGE3),
10001002
])
10011003
def test_hash_encodings(self, test_num, item, encodings, ignore_encoding_errors, expected_result, expected_message):
1002-
if UnicodeDecodeError == expected_result:
1004+
if UnicodeDecodeError == expected_result: # NOQA
10031005
with pytest.raises(expected_result) as exc_info:
10041006
DeepHash(item, encodings=encodings, ignore_encoding_errors=ignore_encoding_errors)
10051007
assert expected_message == str(exc_info.value), f"test_encodings test #{test_num} failed."
10061008
else:
10071009
result = DeepHash(item, encodings=encodings, ignore_encoding_errors=ignore_encoding_errors)
10081010
assert expected_result == result, f"test_encodings test #{test_num} failed."
1011+
1012+
def test_ip_addresses(self):
1013+
1014+
class ClassWithIp:
1015+
"""Class containing single data member to demonstrate deepdiff infinite iterate over IPv6Interface"""
1016+
1017+
def __init__(self, addr: str):
1018+
self.field: Union[
1019+
ipaddress.IPv4Network,
1020+
ipaddress.IPv6Network,
1021+
ipaddress.IPv4Interface,
1022+
ipaddress.IPv6Interface,
1023+
] = ipaddress.IPv6Network(addr)
1024+
1025+
1026+
obj1 = ClassWithIp("2002:db8::/30")
1027+
obj1_hash = DeepHashPrep(obj1)
1028+
repr(obj1_hash) # shouldn't raise error
1029+
assert r"objClassWithIp:{str:field:iprange:2002:db8::/30}" == obj1_hash[obj1]
1030+
obj2 = ClassWithIp("2001:db8::/32")
1031+
diff = DeepDiff(obj1, obj2)
1032+
assert {
1033+
"values_changed": {
1034+
"root.field": {
1035+
"new_value": ipaddress.IPv6Network("2001:db8::/32"),
1036+
"old_value": ipaddress.IPv6Network("2002:db8::/30"),
1037+
}
1038+
}
1039+
} == diff

0 commit comments

Comments
 (0)