Skip to content

Commit 5f25cc5

Browse files
committed
adding use_enum_value and threshold_to_diff_deeper
1 parent b391ae9 commit 5f25cc5

File tree

6 files changed

+93
-47
lines changed

6 files changed

+93
-47
lines changed

deepdiff/deephash.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ def __init__(self,
139139
ignore_numeric_type_changes=False,
140140
ignore_type_subclasses=False,
141141
ignore_string_case=False,
142+
use_enum_value=False,
142143
exclude_obj_callback=None,
143144
number_to_string_func=None,
144145
ignore_private_variables=True,
@@ -154,7 +155,7 @@ def __init__(self,
154155
"exclude_paths, include_paths, exclude_regex_paths, hasher, ignore_repetition, "
155156
"number_format_notation, apply_hash, ignore_type_in_groups, ignore_string_type_changes, "
156157
"ignore_numeric_type_changes, ignore_type_subclasses, ignore_string_case "
157-
"number_to_string_func, ignore_private_variables, parent "
158+
"number_to_string_func, ignore_private_variables, parent, use_enum_value "
158159
"encodings, ignore_encoding_errors") % ', '.join(kwargs.keys()))
159160
if isinstance(hashes, MutableMapping):
160161
self.hashes = hashes
@@ -170,6 +171,7 @@ def __init__(self,
170171
self.exclude_regex_paths = convert_item_or_items_into_compiled_regexes_else_none(exclude_regex_paths)
171172
self.hasher = default_hasher if hasher is None else hasher
172173
self.hashes[UNPROCESSED_KEY] = []
174+
self.use_enum_value = use_enum_value
173175

174176
self.significant_digits = self.get_significant_digits(significant_digits, ignore_numeric_type_changes)
175177
self.truncate_datetime = get_truncate_datetime(truncate_datetime)
@@ -206,10 +208,10 @@ def __init__(self,
206208
sha1hex = sha1hex
207209

208210
def __getitem__(self, obj, extract_index=0):
209-
return self._getitem(self.hashes, obj, extract_index=extract_index)
211+
return self._getitem(self.hashes, obj, extract_index=extract_index, use_enum_value=self.use_enum_value)
210212

211213
@staticmethod
212-
def _getitem(hashes, obj, extract_index=0):
214+
def _getitem(hashes, obj, extract_index=0, use_enum_value=False):
213215
"""
214216
extract_index is zero for hash and 1 for count and None to get them both.
215217
To keep it backward compatible, we only get the hash by default so it is set to zero by default.
@@ -220,6 +222,8 @@ def _getitem(hashes, obj, extract_index=0):
220222
key = BoolObj.TRUE
221223
elif obj is False:
222224
key = BoolObj.FALSE
225+
elif use_enum_value and isinstance(obj, Enum):
226+
key = obj.value
223227

224228
result_n_count = (None, 0)
225229

@@ -256,14 +260,14 @@ def get(self, key, default=None, extract_index=0):
256260
return self.get_key(self.hashes, key, default=default, extract_index=extract_index)
257261

258262
@staticmethod
259-
def get_key(hashes, key, default=None, extract_index=0):
263+
def get_key(hashes, key, default=None, extract_index=0, use_enum_value=False):
260264
"""
261265
get_key method for the hashes dictionary.
262266
It can extract the hash for a given key that is already calculated when extract_index=0
263267
or the count of items that went to building the object whenextract_index=1.
264268
"""
265269
try:
266-
result = DeepHash._getitem(hashes, key, extract_index=extract_index)
270+
result = DeepHash._getitem(hashes, key, extract_index=extract_index, use_enum_value=use_enum_value)
267271
except KeyError:
268272
result = default
269273
return result
@@ -481,6 +485,8 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
481485
if isinstance(obj, bool):
482486
obj = self._prep_bool(obj)
483487
result = None
488+
elif self.use_enum_value and isinstance(obj, Enum):
489+
obj = obj.value
484490
else:
485491
result = not_hashed
486492
try:

deepdiff/diff.py

+25-31
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ def _report_progress(_stats, progress_logger, duration):
9898
'number_format_notation',
9999
'ignore_string_type_changes',
100100
'ignore_numeric_type_changes',
101+
'use_enum_value',
101102
'ignore_type_in_groups',
102103
'ignore_type_subclasses',
103104
'ignore_string_case',
@@ -116,6 +117,7 @@ class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, Base):
116117
def __init__(self,
117118
t1: Any,
118119
t2: Any,
120+
_original_type=None,
119121
cache_purge_level: int=1,
120122
cache_size: int=0,
121123
cache_tuning_sample_size: int=0,
@@ -126,9 +128,6 @@ def __init__(self,
126128
exclude_obj_callback: Optional[Callable]=None,
127129
exclude_obj_callback_strict: Optional[Callable]=None,
128130
exclude_paths: Union[str, List[str]]=None,
129-
include_obj_callback: Optional[Callable]=None,
130-
include_obj_callback_strict: Optional[Callable]=None,
131-
include_paths: Union[str, List[str]]=None,
132131
exclude_regex_paths: Union[str, List[str], Pattern[str], List[Pattern[str]], None]=None,
133132
exclude_types: Optional[List[Any]]=None,
134133
get_deep_distance: bool=False,
@@ -146,8 +145,10 @@ def __init__(self,
146145
ignore_string_type_changes: bool=False,
147146
ignore_type_in_groups: Optional[List[Tuple]]=None,
148147
ignore_type_subclasses: bool=False,
148+
include_obj_callback: Optional[Callable]=None,
149+
include_obj_callback_strict: Optional[Callable]=None,
150+
include_paths: Union[str, List[str]]=None,
149151
iterable_compare_func: Optional[Callable]=None,
150-
zip_ordered_iterables: bool=False,
151152
log_frequency_in_sec: int=0,
152153
math_epsilon: Optional[float]=None,
153154
max_diffs: Optional[int]=None,
@@ -157,10 +158,12 @@ def __init__(self,
157158
progress_logger: Callable=logger.info,
158159
report_repetition: bool=False,
159160
significant_digits: Optional[int]=None,
161+
threshold_to_diff_deeper: float = 0,
160162
truncate_datetime: Optional[str]=None,
163+
use_enum_value: bool=False,
161164
verbose_level: int=1,
162165
view: str=TEXT_VIEW,
163-
_original_type=None,
166+
zip_ordered_iterables: bool=False,
164167
_parameters=None,
165168
_shared_parameters=None,
166169
**kwargs):
@@ -175,7 +178,7 @@ def __init__(self,
175178
"view, hasher, hashes, max_passes, max_diffs, zip_ordered_iterables, "
176179
"cutoff_distance_for_pairs, cutoff_intersection_for_pairs, log_frequency_in_sec, cache_size, "
177180
"cache_tuning_sample_size, get_deep_distance, group_by, group_by_sort_key, cache_purge_level, "
178-
"math_epsilon, iterable_compare_func, _original_type, "
181+
"math_epsilon, iterable_compare_func, use_enum_value, _original_type, threshold_to_diff_deeper, "
179182
"ignore_order_func, custom_operators, encodings, ignore_encoding_errors, "
180183
"_parameters and _shared_parameters.") % ', '.join(kwargs.keys()))
181184

@@ -193,6 +196,8 @@ def __init__(self,
193196
self.ignore_numeric_type_changes = ignore_numeric_type_changes
194197
if strings == ignore_type_in_groups or strings in ignore_type_in_groups:
195198
ignore_string_type_changes = True
199+
self.use_enum_value = use_enum_value
200+
self.threshold_to_diff_deeper = threshold_to_diff_deeper
196201
self.ignore_string_type_changes = ignore_string_type_changes
197202
self.ignore_type_in_groups = self.get_ignore_types_in_groups(
198203
ignore_type_in_groups=ignore_type_in_groups,
@@ -513,6 +518,8 @@ def _get_clean_to_keys_mapping(self, keys, level):
513518
for key in keys:
514519
if self.ignore_string_type_changes and isinstance(key, bytes):
515520
clean_key = key.decode('utf-8')
521+
elif self.use_enum_value and isinstance(key, Enum):
522+
clean_key = key.value
516523
elif isinstance(key, numbers):
517524
type_ = "number" if self.ignore_numeric_type_changes else key.__class__.__name__
518525
clean_key = self.number_to_string(key, significant_digits=self.significant_digits,
@@ -578,6 +585,12 @@ def _diff_dict(
578585
t_keys_added = t2_keys - t_keys_intersect
579586
t_keys_removed = t1_keys - t_keys_intersect
580587

588+
if self.threshold_to_diff_deeper:
589+
len_keys_changed = (len(t_keys_added) + len(t_keys_removed))
590+
if len_keys_changed and len(t_keys_intersect) / len_keys_changed < self.threshold_to_diff_deeper:
591+
self._report_result('values_changed', level, local_tree=local_tree)
592+
return
593+
581594
for key in t_keys_added:
582595
if self._count_diff() is StopIteration:
583596
return
@@ -861,31 +874,6 @@ def _diff_by_forming_pairs_and_comparing_one_by_one(
861874
self._report_result('iterable_item_added', change_level, local_tree=local_tree)
862875

863876
else: # check if item value has changed
864-
865-
# if (i != j):
866-
# # Item moved
867-
# change_level = level.branch_deeper(
868-
# x,
869-
# y,
870-
# child_relationship_class=child_relationship_class,
871-
# child_relationship_param=i,
872-
# child_relationship_param2=j
873-
# )
874-
# self._report_result('iterable_item_moved', change_level)
875-
876-
# item_id = id(x)
877-
# if parents_ids and item_id in parents_ids:
878-
# continue
879-
# parents_ids_added = add_to_frozen_set(parents_ids, item_id)
880-
881-
# # Go one level deeper
882-
# next_level = level.branch_deeper(
883-
# x,
884-
# y,
885-
# child_relationship_class=child_relationship_class,
886-
# child_relationship_param=j)
887-
# self._diff(next_level, parents_ids_added)
888-
889877
if (i != j and ((x == y) or self.iterable_compare_func)):
890878
# Item moved
891879
change_level = level.branch_deeper(
@@ -1604,6 +1592,12 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=
16041592
if self.type_check_func(level.t1, type_group) and self.type_check_func(level.t2, type_group):
16051593
report_type_change = False
16061594
break
1595+
if self.use_enum_value and isinstance(level.t1, Enum):
1596+
level.t1 = level.t1.value
1597+
report_type_change = False
1598+
if self.use_enum_value and isinstance(level.t2, Enum):
1599+
level.t2 = level.t2.value
1600+
report_type_change = False
16071601
if report_type_change:
16081602
self._diff_types(level, local_tree=local_tree)
16091603
return

tests/__init__.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,13 @@ def __reduce__(self):
6565
return (self.__class__, (item, ))
6666

6767
def __eq__(self, other):
68-
both_no_items_attr = (not hasattr(self, 'item')) and (not hasattr(other, 'item'))
69-
return both_no_items_attr or self.item == other.item
68+
if hasattr(self, 'item') and hasattr(other, 'item'):
69+
return self.item == other.item
70+
if not hasattr(self, 'item') and not hasattr(other, 'item'):
71+
return True
72+
return False
73+
74+
def __str__(self):
75+
return f"<Picklable: {self.item if hasattr(self, 'item') else 'delete'}>"
76+
77+
__repr__ = __str__

tests/test_delta.py

+16
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,14 @@ def test_delta_dict_items_added_retain_order(self):
463463
delta2 = Delta(diff=diff, bidirectional=True)
464464
assert t1 == t2 - delta2
465465

466+
delta3 = Delta(diff, always_include_values=True, bidirectional=True, raise_errors=True)
467+
flat_rows_list = delta3.to_flat_rows()
468+
delta4 = Delta(flat_rows_list=flat_rows_list,
469+
always_include_values=True, bidirectional=True, raise_errors=True)
470+
assert t1 == t2 - delta4
471+
assert t1 + delta4 == t2
472+
473+
466474
def test_delta_constr_flat_dict_list_param_preserve(self):
467475
"""
468476
Issue: https://github.com/seperman/deepdiff/issues/457
@@ -818,6 +826,13 @@ def compare_func(item1, item2, level=None):
818826
}
819827
}
820828
},
829+
'delta_case14b_threshold_to_diff_deeper': {
830+
't1': picklalbe_obj_without_item,
831+
't2': PicklableClass(11),
832+
'deepdiff_kwargs': {'threshold_to_diff_deeper': 0.33},
833+
'to_delta_kwargs': {},
834+
'expected_delta_dict': {'values_changed': {'root': {'new_value': PicklableClass(11)}}}
835+
},
821836
'delta_case15_diffing_simple_numbers': {
822837
't1': 1,
823838
't2': 2,
@@ -1451,6 +1466,7 @@ def test_delta_view_and_to_delta_dict_are_equal_when_parameteres_passed(self):
14511466
'ignore_string_type_changes': False,
14521467
'ignore_type_in_groups': [],
14531468
'report_repetition': True,
1469+
'use_enum_value': False,
14541470
'exclude_paths': None,
14551471
'include_paths': None,
14561472
'exclude_regex_paths': None,

tests/test_diff_text.py

+29-9
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,16 @@
1616
logging.disable(logging.CRITICAL)
1717

1818

19+
class MyEnum1(Enum):
20+
book = "book"
21+
cake = "cake"
22+
23+
class MyEnum2(str, Enum):
24+
book = "book"
25+
cake = "cake"
26+
27+
28+
1929
class TestDeepDiffText:
2030
"""DeepDiff Tests."""
2131

@@ -649,14 +659,6 @@ class MyEnum(Enum):
649659

650660
def test_enum_ignore_type_change(self):
651661

652-
class MyEnum1(Enum):
653-
book = "book"
654-
cake = "cake"
655-
656-
class MyEnum2(str, Enum):
657-
book = "book"
658-
cake = "cake"
659-
660662
diff = DeepDiff("book", MyEnum1.book)
661663
expected = {
662664
'type_changes': {'root': {'old_type': str, 'new_type': MyEnum1, 'old_value': 'book', 'new_value': MyEnum1.book}}}
@@ -668,6 +670,14 @@ class MyEnum2(str, Enum):
668670
diff3 = DeepDiff("book", MyEnum2.book, ignore_type_in_groups=[(Enum, str)])
669671
assert not diff3
670672

673+
def test_enum_use_enum_value1(self):
674+
diff = DeepDiff("book", MyEnum2.book, use_enum_value=True)
675+
assert not diff
676+
677+
def test_enum_use_enum_value_in_dict_key(self):
678+
diff = DeepDiff({"book": 2}, {MyEnum2.book: 2}, use_enum_value=True)
679+
assert not diff
680+
671681
def test_precompiled_regex(self):
672682

673683
pattern_1 = re.compile('foo')
@@ -950,6 +960,9 @@ def test_custom_objects_add_and_remove_verbose(self):
950960

951961
def get_custom_object_with_added_removed_methods(self):
952962
class ClassA:
963+
VAL = 1
964+
VAL2 = 2
965+
953966
def method_a(self):
954967
pass
955968

@@ -1000,14 +1013,21 @@ def test_dictionary_of_custom_objects(self):
10001013
result = {}
10011014
assert result == ddiff
10021015

1003-
def test_dictionary_with_string_keys(self):
1016+
def test_dictionary_with_string_keys1(self):
10041017
t1 = {"veggie": "carrots"}
10051018
t2 = {"meat": "carrots"}
10061019

10071020
diff = DeepDiff(t1, t2)
10081021
assert {'dictionary_item_added': ["root['meat']"],
10091022
'dictionary_item_removed': ["root['veggie']"]} == diff
10101023

1024+
def test_dictionary_with_string_keys_threshold_to_diff_deeper(self):
1025+
t1 = {"veggie": "carrots"}
1026+
t2 = {"meat": "carrots"}
1027+
1028+
diff = DeepDiff(t1, t2, threshold_to_diff_deeper=0.33)
1029+
assert {'values_changed': {'root': {'new_value': {'meat': 'carrots'}, 'old_value': {'veggie': 'carrots'}}}} == diff
1030+
10111031
def test_dictionary_with_numeric_keys(self):
10121032
t1 = {Decimal('10.01'): "carrots"}
10131033
t2 = {10.01: "carrots"}

tests/test_hash.py

+2
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,8 @@ class MyEnum(Enum):
310310
assert DeepHashPrep(MyEnum.A) != DeepHashPrep(MyEnum.A.value)
311311
assert DeepHashPrep(MyEnum.A) != DeepHashPrep(MyEnum.B)
312312

313+
assert DeepHashPrep(MyEnum.A, use_enum_value=True)[MyEnum.A] == 'int:1'
314+
313315
def test_dict_hash(self):
314316
string1 = "a"
315317
string1_prepped = prep_str(string1)

0 commit comments

Comments
 (0)