Skip to content

Commit 9c8968f

Browse files
authored
Merge pull request #483 from seperman/dev
8.1.0
2 parents 6d8a4c7 + d2d3806 commit 9c8968f

24 files changed

+737
-137
lines changed

.github/workflows/main.yaml

+9-9
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-latest
1313
strategy:
1414
matrix:
15-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
15+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
1616
architecture: ["x64"]
1717
steps:
1818
- uses: actions/checkout@v2
@@ -44,34 +44,34 @@ jobs:
4444
${{ runner.os }}-pip-
4545
${{ runner.os }}-
4646
- name: Upgrade setuptools
47-
if: matrix.python-version == 3.12
47+
if: matrix.python-version => 3.12
4848
run: |
49-
# workaround for 3.12, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177
49+
# workaround for 3.13, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177
5050
pip install --upgrade setuptools
5151
- name: Install dependencies
52-
if: matrix.python-version != 3.8
52+
if: matrix.python-version > 3.9
5353
run: pip install -r requirements-dev.txt
5454
- name: Install dependencies
55-
if: matrix.python-version == 3.8
55+
if: matrix.python-version <= 3.9
5656
run: pip install -r requirements-dev3.8.txt
5757
- name: Lint with flake8
58-
if: matrix.python-version == 3.12
58+
if: matrix.python-version == 3.13
5959
run: |
6060
# stop the build if there are Python syntax errors or undefined names
6161
flake8 deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
6262
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
6363
flake8 deepdiff --count --exit-zero --max-complexity=26 --max-line-lengt=250 --statistics
6464
- name: Test with pytest and get the coverage
65-
if: matrix.python-version == 3.12
65+
if: matrix.python-version == 3.13
6666
run: |
6767
pytest --benchmark-disable --cov-report=xml --cov=deepdiff tests/ --runslow
6868
- name: Test with pytest and no coverage report
69-
if: matrix.python-version != 3.12
69+
if: matrix.python-version != 3.13
7070
run: |
7171
pytest --benchmark-disable
7272
- name: Upload coverage to Codecov
7373
uses: codecov/codecov-action@v4
74-
if: matrix.python-version == 3.12
74+
if: matrix.python-version == 3.13
7575
env:
7676
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
7777
with:

AUTHORS.md

+9
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,12 @@ Authors in order of the timeline of their contributions:
6363
- [sf-tcalhoun](https://github.com/sf-tcalhoun) for fixing "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list"
6464
- [dtorres-sf](https://github.com/dtorres-sf) for fixing iterable moved items when iterable_compare_func is used.
6565
- [Florian Finkernagel](https://github.com/TyberiusPrime) for pandas and polars support.
66+
- Mathis Chenuet [artemisart](https://github.com/artemisart) for fixing slots classes comparison and PR review.
67+
- Sherjeel Shabih [sherjeelshabih](https://github.com/sherjeelshabih) for fixing the issue where the key deep_distance is not returned when both compared items are equal #510
68+
- [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding `prefix` option to `pretty()`
69+
- [Juergen Skrotzky](https://github.com/Jorgen-VikingGod) for adding empty `py.typed`
70+
- [Mate Valko](https://github.com/vmatt) for fixing the issue so we lower only if clean_key is instance of str via #504
71+
- [jlaba](https://github.com/jlaba) for fixing #493 include_paths, when only certain keys are included via #499
72+
- [Doron Behar](https://github.com/doronbehar) for fixing DeepHash for numpy booleans via #496
73+
- [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding print() options which allows a user-defined string (or callback function) to prefix every output when using the pretty() call.
74+
- [David Hotham](https://github.com/dimbleby) for relaxing orderly-set dependency via #486

CHANGELOG.md

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
# DeepDiff Change log
22

33

4+
- v8-1-0
5+
- Removing deprecated lines from setup.py
6+
- Added `prefix` option to `pretty()`
7+
- Fixes hashing of numpy boolean values.
8+
- Fixes __slots__ comparison when the attribute doesn't exist.
9+
- Relaxing orderly-set reqs
10+
- Added Python 3.13 support
11+
- Only lower if clean_key is instance of str
12+
- Only lower if clean_key is instance of str #504
13+
- Fixes issue where the key deep_distance is not returned when both compared items are equal
14+
- Fixes issue where the key deep_distance is not returned when both compared items are equal #510
15+
- Fixes exclude_paths fails to work in certain cases
16+
- exclude_paths fails to work #509
17+
- Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys
18+
- to_dict() method chokes on standard json.dumps() kwargs #490
19+
- Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty
20+
- In version 8.0.1, accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508
21+
22+
423
- v8-0-1
524
- Bugfix. Numpy should be optional.
625

README.md

+19
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ Tested on Python 3.8+ and PyPy3.
2323

2424
Please check the [ChangeLog](CHANGELOG.md) file for the detailed information.
2525

26+
DeepDiff 8-1-0
27+
28+
- Removing deprecated lines from setup.py
29+
- Added `prefix` option to `pretty()`
30+
- Fixes hashing of numpy boolean values.
31+
- Fixes __slots__ comparison when the attribute doesn't exist.
32+
- Relaxing orderly-set reqs
33+
- Added Python 3.13 support
34+
- Only lower if clean_key is instance of str
35+
- Only lower if clean_key is instance of str #504
36+
- Fixes issue where the key deep_distance is not returned when both compared items are equal
37+
- Fixes issue where the key deep_distance is not returned when both compared items are equal #510
38+
- Fixes exclude_paths fails to work in certain cases
39+
- exclude_paths fails to work #509
40+
- Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys
41+
- to_dict() method chokes on standard json.dumps() kwargs #490
42+
- Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty
43+
- In version 8.0.1, accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508
44+
2645
DeepDiff 8-0-1
2746

2847
- Bugfix. Numpy should be optional.

deepdiff/deephash.py

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
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, short_repr,
15-
get_truncate_datetime, dict_, add_root_to_paths)
15+
get_truncate_datetime, dict_, add_root_to_paths, PydanticBaseModel)
1616
from deepdiff.base import Base
1717

1818
try:
@@ -24,6 +24,11 @@
2424
import polars
2525
except ImportError:
2626
polars = False
27+
try:
28+
import numpy as np
29+
booleanTypes = (bool, np.bool_)
30+
except ImportError:
31+
booleanTypes = bool
2732

2833
logger = logging.getLogger(__name__)
2934

@@ -326,13 +331,15 @@ def values(self):
326331
def items(self):
327332
return ((i, v[0]) for i, v in self.hashes.items())
328333

329-
def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False):
334+
def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False, is_pydantic_object=False):
330335
"""prepping objects"""
331336
original_type = type(obj) if not isinstance(obj, type) else obj
332337

333338
obj_to_dict_strategies = []
334339
if is_namedtuple:
335340
obj_to_dict_strategies.append(lambda o: o._asdict())
341+
elif is_pydantic_object:
342+
obj_to_dict_strategies.append(lambda o: {k: v for (k, v) in o.__dict__.items() if v !="model_fields_set"})
336343
else:
337344
obj_to_dict_strategies.append(lambda o: o.__dict__)
338345

@@ -492,7 +499,7 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
492499
"""The main hash method"""
493500
counts = 1
494501

495-
if isinstance(obj, bool):
502+
if isinstance(obj, booleanTypes):
496503
obj = self._prep_bool(obj)
497504
result = None
498505
elif self.use_enum_value and isinstance(obj, Enum):
@@ -557,6 +564,8 @@ def gen():
557564

558565
elif obj == BoolObj.TRUE or obj == BoolObj.FALSE:
559566
result = 'bool:true' if obj is BoolObj.TRUE else 'bool:false'
567+
elif isinstance(obj, PydanticBaseModel):
568+
result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids, is_pydantic_object=True)
560569
else:
561570
result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids)
562571

deepdiff/diff.py

+54-12
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def _report_progress(_stats, progress_logger, duration):
8080
PURGE_LEVEL_RANGE_MSG = 'cache_purge_level should be 0, 1, or 2.'
8181
_ENABLE_CACHE_EVERY_X_DIFF = '_ENABLE_CACHE_EVERY_X_DIFF'
8282

83+
model_fields_set = frozenset(["model_fields_set"])
84+
85+
8386
# What is the threshold to consider 2 items to be pairs. Only used when ignore_order = True.
8487
CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT = 0.3
8588

@@ -421,7 +424,7 @@ def unmangle(attribute):
421424
else:
422425
all_slots.extend(slots)
423426

424-
return {i: getattr(object, unmangle(i)) for i in all_slots}
427+
return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))}
425428

426429
def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None):
427430
t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS)
@@ -437,13 +440,16 @@ def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None):
437440
local_tree=local_tree,
438441
)
439442

440-
def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None):
443+
def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None, is_pydantic_object=False):
441444
"""Difference of 2 objects"""
442445
processing_error = False
443446
try:
444447
if is_namedtuple:
445448
t1 = level.t1._asdict()
446449
t2 = level.t2._asdict()
450+
elif is_pydantic_object:
451+
t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set)
452+
t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set)
447453
elif all('__dict__' in dir(t) for t in level):
448454
t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables)
449455
t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables)
@@ -510,6 +516,32 @@ def _skip_this(self, level):
510516

511517
return skip
512518

519+
def _skip_this_key(self, level, key):
520+
# if include_paths is not set, than treet every path as included
521+
if self.include_paths is None:
522+
return False
523+
if "{}['{}']".format(level.path(), key) in self.include_paths:
524+
return False
525+
if level.path() in self.include_paths:
526+
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"]
527+
return False
528+
for prefix in self.include_paths:
529+
if "{}['{}']".format(level.path(), key) in prefix:
530+
# matches as long the prefix is longer than this object key
531+
# eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths
532+
# level+key root['foo'] matches prefix root['foo']['bar'] from include_paths
533+
# level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards
534+
return False
535+
# check if a higher level is included as a whole (=without any sublevels specified)
536+
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"]
537+
# but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"]
538+
up = level.up
539+
while up is not None:
540+
if up.path() in self.include_paths:
541+
return False
542+
up = up.up
543+
return True
544+
513545
def _get_clean_to_keys_mapping(self, keys, level):
514546
"""
515547
Get a dictionary of cleaned value of keys to the keys themselves.
@@ -530,7 +562,7 @@ def _get_clean_to_keys_mapping(self, keys, level):
530562
clean_key = KEY_TO_VAL_STR.format(type_, clean_key)
531563
else:
532564
clean_key = key
533-
if self.ignore_string_case:
565+
if self.ignore_string_case and isinstance(clean_key, str):
534566
clean_key = clean_key.lower()
535567
if clean_key in result:
536568
logger.warning(('{} and {} in {} become the same key when ignore_numeric_type_changes'
@@ -570,11 +602,11 @@ def _diff_dict(
570602
rel_class = DictRelationship
571603

572604
if self.ignore_private_variables:
573-
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__'))])
574-
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__'))])
605+
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
606+
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
575607
else:
576-
t1_keys = SetOrdered(t1.keys())
577-
t2_keys = SetOrdered(t2.keys())
608+
t1_keys = SetOrdered([key for key in t1 if not self._skip_this_key(level, key)])
609+
t2_keys = SetOrdered([key for key in t2 if not self._skip_this_key(level, key)])
578610
if self.ignore_string_type_changes or self.ignore_numeric_type_changes or self.ignore_string_case:
579611
t1_clean_to_keys = self._get_clean_to_keys_mapping(keys=t1_keys, level=level)
580612
t2_clean_to_keys = self._get_clean_to_keys_mapping(keys=t2_keys, level=level)
@@ -584,11 +616,17 @@ def _diff_dict(
584616
t1_clean_to_keys = t2_clean_to_keys = None
585617

586618
t_keys_intersect = t2_keys & t1_keys
587-
t_keys_union = t2_keys | t1_keys
588619
t_keys_added = t2_keys - t_keys_intersect
589620
t_keys_removed = t1_keys - t_keys_intersect
621+
590622
if self.threshold_to_diff_deeper:
591-
if len(t_keys_union) > 1 and len(t_keys_intersect) / len(t_keys_union) < self.threshold_to_diff_deeper:
623+
if self.exclude_paths:
624+
t_keys_union = {f"{level.path()}[{repr(key)}]" for key in (t2_keys | t1_keys)}
625+
t_keys_union -= self.exclude_paths
626+
t_keys_union_len = len(t_keys_union)
627+
else:
628+
t_keys_union_len = len(t2_keys | t1_keys)
629+
if t_keys_union_len > 1 and len(t_keys_intersect) / t_keys_union_len < self.threshold_to_diff_deeper:
592630
self._report_result('values_changed', level, local_tree=local_tree)
593631
return
594632

@@ -1652,7 +1690,7 @@ def _diff(self, level, parents_ids=frozenset(), _original_type=None, local_tree=
16521690
self._diff_numpy_array(level, parents_ids, local_tree=local_tree)
16531691

16541692
elif isinstance(level.t1, PydanticBaseModel):
1655-
self._diff_obj(level, parents_ids, local_tree=local_tree)
1693+
self._diff_obj(level, parents_ids, local_tree=local_tree, is_pydantic_object=True)
16561694

16571695
elif isinstance(level.t1, Iterable):
16581696
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
@@ -1808,9 +1846,13 @@ def affected_root_keys(self):
18081846
value = self.tree.get(key)
18091847
if value:
18101848
if isinstance(value, SetOrdered):
1811-
result |= SetOrdered([i.get_root_key() for i in value])
1849+
values_list = value
18121850
else:
1813-
result |= SetOrdered([i.get_root_key() for i in value.keys()])
1851+
values_list = value.keys()
1852+
for item in values_list:
1853+
root_key = item.get_root_key()
1854+
if root_key is not notpresent:
1855+
result.add(root_key)
18141856
return result
18151857

18161858

deepdiff/model.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def remove_empty_keys(self):
4141
Remove empty keys from this object. Should always be called after the result is final.
4242
:return:
4343
"""
44-
empty_keys = [k for k, v in self.items() if not v]
44+
empty_keys = [k for k, v in self.items() if not isinstance(v, (int)) and not v]
4545

4646
for k in empty_keys:
4747
del self[k]
@@ -88,7 +88,13 @@ def __getitem__(self, item):
8888
return self.get(item)
8989

9090
def __len__(self):
91-
return sum([len(i) for i in self.values() if isinstance(i, SetOrdered)])
91+
length = 0
92+
for value in self.values():
93+
if isinstance(value, SetOrdered):
94+
length += len(value)
95+
elif isinstance(value, int):
96+
length += 1
97+
return length
9298

9399

94100
class TextResult(ResultDict):
@@ -659,7 +665,9 @@ def get_root_key(self, use_t2=False):
659665
else:
660666
next_rel = root_level.t1_child_rel or root_level.t2_child_rel # next relationship object to get a formatted param from
661667

662-
return next_rel.param
668+
if next_rel:
669+
return next_rel.param
670+
return notpresent
663671

664672
def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'):
665673
"""

deepdiff/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)