Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8.1.0 #483

Merged
merged 42 commits into from
Dec 16, 2024
Merged

8.1.0 #483

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
cc30a3c
Fix _dict_from_slots, solves Path comparison
artemisart Sep 3, 2024
c1161b3
use hasattr instead of getattr None
artemisart Sep 3, 2024
47d7816
Removing deprecated attributes from setup.py
seperman Sep 11, 2024
38ac719
no diff anymore
artemisart Sep 12, 2024
ce1c8fb
add author
artemisart Sep 12, 2024
a114ed2
Merge pull request #480 from artemisart/patch-1
seperman Sep 12, 2024
5797841
relax orderly-set dependency
dimbleby Sep 15, 2024
ae50b1c
Merge branch 'dev' into relax-requirement
seperman Sep 29, 2024
111a2eb
Merge pull request #486 from dimbleby/relax-requirement
seperman Sep 29, 2024
5f22bd2
Add print() option
AaronDMarasco Oct 10, 2024
cdc4b30
Merge pull request #492 from AaronDMarasco/dev
seperman Oct 13, 2024
32df472
DeepHash: check numpy booleans like native booleans
doronbehar Oct 19, 2024
cee3d41
TestDeepHash: test numpy booleans
doronbehar Oct 20, 2024
5d30b3a
Merge pull request #496 from doronbehar/fix-np.bool_
seperman Oct 25, 2024
7bb48a1
Added missing suffix of tests/test_diff_include_paths_root.py
Oct 26, 2024
916f02f
Added tests for wrong diff result with include_paths and changed number
Oct 26, 2024
fc8baaa
Fixed include_paths fault, if only certain keys of a path are included
Oct 26, 2024
32d60a9
Merge pull request #499 from jlaba/master
seperman Nov 14, 2024
2d61bb1
updating dev dependencies. Adding tests for include_paths
seperman Nov 14, 2024
269a971
Merge branch 'dev' of github.com:seperman/deepdiff into dev
seperman Nov 14, 2024
f6c7bcb
Only lower if clean_key is instance of str
vmatt Nov 18, 2024
360c2f2
Add empty py.typed
Nov 18, 2024
d1c8f90
adding 2 more tests
seperman Dec 6, 2024
fe9fa86
adding python 3.13
seperman Dec 6, 2024
31d7275
Update CHANGELOG.md
seperman Dec 6, 2024
6d819f0
fixing the tests for old pythons
seperman Dec 6, 2024
2f290fe
upgrading dependencies
seperman Dec 6, 2024
151dbdd
only limit to 3.12 to check faster for the issue
seperman Dec 6, 2024
d7e2a94
somehow git actions didn't work. reverting.
seperman Dec 6, 2024
f86033f
Merge pull request #507 from Jorgen-VikingGod/add-py-typed
seperman Dec 6, 2024
85adbd2
add tests for group_by None cases
Dec 7, 2024
324aad3
Fixes __len__ of TreeResult when only comparing un-nested types
sherjeelshabih Dec 9, 2024
514e025
Merge pull request #504 from vmatt/patch-1
seperman Dec 14, 2024
743c901
Merge pull request #510 from sherjeelshabih/dev
seperman Dec 14, 2024
051c6d8
better support for Pydantic models. Ignore model_fields_set when
seperman Dec 14, 2024
767c96c
Merge branch 'dev' of github.com:seperman/deepdiff into dev
seperman Dec 14, 2024
5120230
slight optimization of TreeResult len
seperman Dec 14, 2024
f1d87e9
fixes #509
seperman Dec 15, 2024
42fd42d
fixes to_json() method chokes on some standard json.dumps() such as
seperman Dec 15, 2024
c464e04
fixes accessing the affected_root_keys property on the diff object re…
seperman Dec 15, 2024
737bb5a
updating docs
seperman Dec 16, 2024
d2d3806
fixing types to be compatible for python 3.8
seperman Dec 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
architecture: ["x64"]
steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -44,34 +44,34 @@ jobs:
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Upgrade setuptools
if: matrix.python-version == 3.12
if: matrix.python-version => 3.12
run: |
# workaround for 3.12, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177
# workaround for 3.13, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177
pip install --upgrade setuptools
- name: Install dependencies
if: matrix.python-version != 3.8
if: matrix.python-version > 3.9
run: pip install -r requirements-dev.txt
- name: Install dependencies
if: matrix.python-version == 3.8
if: matrix.python-version <= 3.9
run: pip install -r requirements-dev3.8.txt
- name: Lint with flake8
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 deepdiff --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 deepdiff --count --exit-zero --max-complexity=26 --max-line-lengt=250 --statistics
- name: Test with pytest and get the coverage
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13
run: |
pytest --benchmark-disable --cov-report=xml --cov=deepdiff tests/ --runslow
- name: Test with pytest and no coverage report
if: matrix.python-version != 3.12
if: matrix.python-version != 3.13
run: |
pytest --benchmark-disable
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: matrix.python-version == 3.12
if: matrix.python-version == 3.13
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
Expand Down
9 changes: 9 additions & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,12 @@ Authors in order of the timeline of their contributions:
- [sf-tcalhoun](https://github.com/sf-tcalhoun) for fixing "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list"
- [dtorres-sf](https://github.com/dtorres-sf) for fixing iterable moved items when iterable_compare_func is used.
- [Florian Finkernagel](https://github.com/TyberiusPrime) for pandas and polars support.
- Mathis Chenuet [artemisart](https://github.com/artemisart) for fixing slots classes comparison and PR review.
- 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
- [Aaron D. Marasco](https://github.com/AaronDMarasco) for adding `prefix` option to `pretty()`
- [Juergen Skrotzky](https://github.com/Jorgen-VikingGod) for adding empty `py.typed`
- [Mate Valko](https://github.com/vmatt) for fixing the issue so we lower only if clean_key is instance of str via #504
- [jlaba](https://github.com/jlaba) for fixing #493 include_paths, when only certain keys are included via #499
- [Doron Behar](https://github.com/doronbehar) for fixing DeepHash for numpy booleans via #496
- [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.
- [David Hotham](https://github.com/dimbleby) for relaxing orderly-set dependency via #486
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
# DeepDiff Change log


- v8-1-0
- Removing deprecated lines from setup.py
- Added `prefix` option to `pretty()`
- Fixes hashing of numpy boolean values.
- Fixes __slots__ comparison when the attribute doesn't exist.
- Relaxing orderly-set reqs
- Added Python 3.13 support
- Only lower if clean_key is instance of str
- Only lower if clean_key is instance of str #504
- Fixes issue where the key deep_distance is not returned when both compared items are equal
- Fixes issue where the key deep_distance is not returned when both compared items are equal #510
- Fixes exclude_paths fails to work in certain cases
- exclude_paths fails to work #509
- Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys
- to_dict() method chokes on standard json.dumps() kwargs #490
- Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty
- 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


- v8-0-1
- Bugfix. Numpy should be optional.

Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ Tested on Python 3.8+ and PyPy3.

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

DeepDiff 8-1-0

- Removing deprecated lines from setup.py
- Added `prefix` option to `pretty()`
- Fixes hashing of numpy boolean values.
- Fixes __slots__ comparison when the attribute doesn't exist.
- Relaxing orderly-set reqs
- Added Python 3.13 support
- Only lower if clean_key is instance of str
- Only lower if clean_key is instance of str #504
- Fixes issue where the key deep_distance is not returned when both compared items are equal
- Fixes issue where the key deep_distance is not returned when both compared items are equal #510
- Fixes exclude_paths fails to work in certain cases
- exclude_paths fails to work #509
- Fixes to_json() method chokes on standard json.dumps() kwargs such as sort_keys
- to_dict() method chokes on standard json.dumps() kwargs #490
- Fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty
- 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

DeepDiff 8-0-1

- Bugfix. Numpy should be optional.
Expand Down
15 changes: 12 additions & 3 deletions deepdiff/deephash.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
convert_item_or_items_into_compiled_regexes_else_none,
get_id, type_is_subclass_of_type_group, type_in_type_group,
number_to_string, datetime_normalize, KEY_TO_VAL_STR, short_repr,
get_truncate_datetime, dict_, add_root_to_paths)
get_truncate_datetime, dict_, add_root_to_paths, PydanticBaseModel)
from deepdiff.base import Base

try:
Expand All @@ -24,6 +24,11 @@
import polars
except ImportError:
polars = False
try:
import numpy as np
booleanTypes = (bool, np.bool_)
except ImportError:
booleanTypes = bool

logger = logging.getLogger(__name__)

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

def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False):
def _prep_obj(self, obj, parent, parents_ids=EMPTY_FROZENSET, is_namedtuple=False, is_pydantic_object=False):
"""prepping objects"""
original_type = type(obj) if not isinstance(obj, type) else obj

obj_to_dict_strategies = []
if is_namedtuple:
obj_to_dict_strategies.append(lambda o: o._asdict())
elif is_pydantic_object:
obj_to_dict_strategies.append(lambda o: {k: v for (k, v) in o.__dict__.items() if v !="model_fields_set"})
else:
obj_to_dict_strategies.append(lambda o: o.__dict__)

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

if isinstance(obj, bool):
if isinstance(obj, booleanTypes):
obj = self._prep_bool(obj)
result = None
elif self.use_enum_value and isinstance(obj, Enum):
Expand Down Expand Up @@ -557,6 +564,8 @@ def gen():

elif obj == BoolObj.TRUE or obj == BoolObj.FALSE:
result = 'bool:true' if obj is BoolObj.TRUE else 'bool:false'
elif isinstance(obj, PydanticBaseModel):
result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids, is_pydantic_object=True)
else:
result, counts = self._prep_obj(obj=obj, parent=parent, parents_ids=parents_ids)

Expand Down
66 changes: 54 additions & 12 deletions deepdiff/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def _report_progress(_stats, progress_logger, duration):
PURGE_LEVEL_RANGE_MSG = 'cache_purge_level should be 0, 1, or 2.'
_ENABLE_CACHE_EVERY_X_DIFF = '_ENABLE_CACHE_EVERY_X_DIFF'

model_fields_set = frozenset(["model_fields_set"])


# What is the threshold to consider 2 items to be pairs. Only used when ignore_order = True.
CUTOFF_DISTANCE_FOR_PAIRS_DEFAULT = 0.3

Expand Down Expand Up @@ -421,7 +424,7 @@ def unmangle(attribute):
else:
all_slots.extend(slots)

return {i: getattr(object, unmangle(i)) for i in all_slots}
return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))}

def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None):
t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS)
Expand All @@ -437,13 +440,16 @@ def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None):
local_tree=local_tree,
)

def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None):
def _diff_obj(self, level, parents_ids=frozenset(), is_namedtuple=False, local_tree=None, is_pydantic_object=False):
"""Difference of 2 objects"""
processing_error = False
try:
if is_namedtuple:
t1 = level.t1._asdict()
t2 = level.t2._asdict()
elif is_pydantic_object:
t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set)
t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables, ignore_keys=model_fields_set)
elif all('__dict__' in dir(t) for t in level):
t1 = detailed__dict__(level.t1, ignore_private_variables=self.ignore_private_variables)
t2 = detailed__dict__(level.t2, ignore_private_variables=self.ignore_private_variables)
Expand Down Expand Up @@ -510,6 +516,32 @@ def _skip_this(self, level):

return skip

def _skip_this_key(self, level, key):
# if include_paths is not set, than treet every path as included
if self.include_paths is None:
return False
if "{}['{}']".format(level.path(), key) in self.include_paths:
return False
if level.path() in self.include_paths:
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"]
return False
for prefix in self.include_paths:
if "{}['{}']".format(level.path(), key) in prefix:
# matches as long the prefix is longer than this object key
# eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths
# level+key root['foo'] matches prefix root['foo']['bar'] from include_paths
# level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards
return False
# check if a higher level is included as a whole (=without any sublevels specified)
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"]
# but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"]
up = level.up
while up is not None:
if up.path() in self.include_paths:
return False
up = up.up
return True

def _get_clean_to_keys_mapping(self, keys, level):
"""
Get a dictionary of cleaned value of keys to the keys themselves.
Expand All @@ -530,7 +562,7 @@ def _get_clean_to_keys_mapping(self, keys, level):
clean_key = KEY_TO_VAL_STR.format(type_, clean_key)
else:
clean_key = key
if self.ignore_string_case:
if self.ignore_string_case and isinstance(clean_key, str):
clean_key = clean_key.lower()
if clean_key in result:
logger.warning(('{} and {} in {} become the same key when ignore_numeric_type_changes'
Expand Down Expand Up @@ -570,11 +602,11 @@ def _diff_dict(
rel_class = DictRelationship

if self.ignore_private_variables:
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__'))])
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__'))])
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
else:
t1_keys = SetOrdered(t1.keys())
t2_keys = SetOrdered(t2.keys())
t1_keys = SetOrdered([key for key in t1 if not self._skip_this_key(level, key)])
t2_keys = SetOrdered([key for key in t2 if not self._skip_this_key(level, key)])
if self.ignore_string_type_changes or self.ignore_numeric_type_changes or self.ignore_string_case:
t1_clean_to_keys = self._get_clean_to_keys_mapping(keys=t1_keys, level=level)
t2_clean_to_keys = self._get_clean_to_keys_mapping(keys=t2_keys, level=level)
Expand All @@ -584,11 +616,17 @@ def _diff_dict(
t1_clean_to_keys = t2_clean_to_keys = None

t_keys_intersect = t2_keys & t1_keys
t_keys_union = t2_keys | t1_keys
t_keys_added = t2_keys - t_keys_intersect
t_keys_removed = t1_keys - t_keys_intersect

if self.threshold_to_diff_deeper:
if len(t_keys_union) > 1 and len(t_keys_intersect) / len(t_keys_union) < self.threshold_to_diff_deeper:
if self.exclude_paths:
t_keys_union = {f"{level.path()}[{repr(key)}]" for key in (t2_keys | t1_keys)}
t_keys_union -= self.exclude_paths
t_keys_union_len = len(t_keys_union)
else:
t_keys_union_len = len(t2_keys | t1_keys)
if t_keys_union_len > 1 and len(t_keys_intersect) / t_keys_union_len < self.threshold_to_diff_deeper:
self._report_result('values_changed', level, local_tree=local_tree)
return

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

elif isinstance(level.t1, PydanticBaseModel):
self._diff_obj(level, parents_ids, local_tree=local_tree)
self._diff_obj(level, parents_ids, local_tree=local_tree, is_pydantic_object=True)

elif isinstance(level.t1, Iterable):
self._diff_iterable(level, parents_ids, _original_type=_original_type, local_tree=local_tree)
Expand Down Expand Up @@ -1808,9 +1846,13 @@ def affected_root_keys(self):
value = self.tree.get(key)
if value:
if isinstance(value, SetOrdered):
result |= SetOrdered([i.get_root_key() for i in value])
values_list = value
else:
result |= SetOrdered([i.get_root_key() for i in value.keys()])
values_list = value.keys()
for item in values_list:
root_key = item.get_root_key()
if root_key is not notpresent:
result.add(root_key)
return result


Expand Down
14 changes: 11 additions & 3 deletions deepdiff/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def remove_empty_keys(self):
Remove empty keys from this object. Should always be called after the result is final.
:return:
"""
empty_keys = [k for k, v in self.items() if not v]
empty_keys = [k for k, v in self.items() if not isinstance(v, (int)) and not v]

for k in empty_keys:
del self[k]
Expand Down Expand Up @@ -88,7 +88,13 @@ def __getitem__(self, item):
return self.get(item)

def __len__(self):
return sum([len(i) for i in self.values() if isinstance(i, SetOrdered)])
length = 0
for value in self.values():
if isinstance(value, SetOrdered):
length += len(value)
elif isinstance(value, int):
length += 1
return length


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

return next_rel.param
if next_rel:
return next_rel.param
return notpresent

def path(self, root="root", force=None, get_parent_too=False, use_t2=False, output_format='str'):
"""
Expand Down
Empty file added deepdiff/py.typed
Empty file.
Loading