From cc30a3c15ef098860157788ed14e3a39f5c9b9e5 Mon Sep 17 00:00:00 2001 From: Mathis Chenuet <9201969+artemisart@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:17:18 +0200 Subject: [PATCH 01/31] Fix _dict_from_slots, solves Path comparison --- deepdiff/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4dfec50..44277f5 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -421,7 +421,7 @@ def unmangle(attribute): else: all_slots.extend(slots) - return {i: getattr(object, unmangle(i)) for i in all_slots} + return {i: getattr(object, unmangle(i), None) for i in all_slots} def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None): t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS) From c1161b348e2c89335015b94064aa1effffb84db4 Mon Sep 17 00:00:00 2001 From: Mathis Chenuet <9201969+artemisart@users.noreply.github.com> Date: Tue, 3 Sep 2024 17:58:50 +0000 Subject: [PATCH 02/31] use hasattr instead of getattr None --- deepdiff/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 44277f5..6437fa3 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -421,7 +421,7 @@ def unmangle(attribute): else: all_slots.extend(slots) - return {i: getattr(object, unmangle(i), None) 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) From 47d7816b07f1a46d1c93e714a546626c7fffb717 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Tue, 10 Sep 2024 22:16:16 -0700 Subject: [PATCH 03/31] Removing deprecated attributes from setup.py --- requirements.txt | 2 +- setup.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 28bbd74..62ba302 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -orderly-set==5.2.2 +orderly-set==5.2.3 diff --git a/setup.py b/setup.py index 7db28b6..e18d83f 100755 --- a/setup.py +++ b/setup.py @@ -37,9 +37,7 @@ def get_reqs(filename): license='MIT', packages=['deepdiff'], zip_safe=True, - test_suite="tests", include_package_data=True, - tests_require=['mock'], long_description=long_description, long_description_content_type='text/markdown', install_requires=reqs, From 38ac719b33855ae3c859da7e107984f33045e236 Mon Sep 17 00:00:00 2001 From: Mathis Chenuet <9201969+artemisart@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:24:41 +0000 Subject: [PATCH 04/31] no diff anymore --- tests/test_diff_text.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index ec6f66b..3e5fcc8 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -1713,7 +1713,7 @@ def __str__(self): t2 = Bad() ddiff = DeepDiff(t1, t2) - result = {'unprocessed': ['root: Bad Object and Bad Object']} + result = {} assert result == ddiff def test_dict_none_item_removed(self): From ce1c8fb389f627b55e007c3c9a3640ab59d5981d Mon Sep 17 00:00:00 2001 From: Mathis Chenuet <9201969+artemisart@users.noreply.github.com> Date: Thu, 12 Sep 2024 21:29:55 +0000 Subject: [PATCH 05/31] add author --- AUTHORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.md b/AUTHORS.md index cd3db13..32ae5fc 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,3 +63,4 @@ 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. From 579784145b3cc289baa19a6857c4b8659d057c4c Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sun, 15 Sep 2024 12:33:38 +0100 Subject: [PATCH 06/31] relax orderly-set dependency --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 28bbd74..640cf14 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -orderly-set==5.2.2 +orderly-set>=5.2.2,<6 From 5f22bd27ad73b62183cc85dd550d669ddb9706e2 Mon Sep 17 00:00:00 2001 From: "Aaron D. Marasco" Date: Wed, 9 Oct 2024 21:40:40 -0400 Subject: [PATCH 07/31] Add print() option --- AUTHORS.md | 1 + CHANGELOG.md | 1 + deepdiff/serialization.py | 8 +++++-- docs/view.rst | 23 ++++++++++++++++++++ tests/test_serialization.py | 43 +++++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 32ae5fc..79d9edb 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -64,3 +64,4 @@ Authors in order of the timeline of their contributions: - [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. +- [Aaron D. Marasco](https://github.com/AaronDMarasco) added `prefix` option to `pretty()` diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cd2c7..12da1c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - v8-0-1 - Bugfix. Numpy should be optional. + - Added `prefix` option to `pretty()` - v8-0-0 diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index 5b4075e..e350b3c 100644 --- a/deepdiff/serialization.py +++ b/deepdiff/serialization.py @@ -296,7 +296,7 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True, always_ return deepcopy(dict(result)) - def pretty(self): + def pretty(self, prefix=None): """ The pretty human readable string output for the diff object regardless of what view was used to generate the diff. @@ -310,12 +310,16 @@ def pretty(self): Item root[1] removed from set. """ result = [] + if prefix is None: + prefix = '' keys = sorted(self.tree.keys()) # sorting keys to guarantee constant order across python versions. for key in keys: for item_key in self.tree[key]: result += [pretty_print_diff(item_key)] - return '\n'.join(result) + if callable(prefix): + return "\n".join(f"{prefix(diff=self)}{r}" for r in result) + return "\n".join(f"{prefix}{r}" for r in result) class _RestrictedUnpickler(pickle.Unpickler): diff --git a/docs/view.rst b/docs/view.rst index f50fc9f..6343590 100644 --- a/docs/view.rst +++ b/docs/view.rst @@ -299,6 +299,29 @@ Use the pretty method for human readable output. This is regardless of what view Item root[4] removed from set. Item root[1] removed from set. +The pretty method has an optional parameter ``prefix`` that allows a prefix string before every output line (*e.g.* for logging): + >>> from deepdiff import DeepDiff + >>> t1={1,2,4} + >>> t2={2,3} + >>> print(DeepDiff(t1, t2).pretty(prefix='Diff: ')) + Diff: Item root[3] added to set. + Diff: Item root[4] removed from set. + Diff: Item root[1] removed from set. + +The ``prefix`` may also be a callable function. This function must accept ``**kwargs``; as of this version, the only parameter is ``diff`` but the signature allows for future expansion. +The ``diff`` given will be the ``DeepDiff`` that ``pretty`` was called on; this allows interesting capabilities such as: + >>> from deepdiff import DeepDiff + >>> t1={1,2,4} + >>> t2={2,3} + >>> def callback(**kwargs): + ... """Helper function using a hidden variable on the diff that tracks which count prints next""" + ... kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) + ... return f"Diff #{kwargs['diff']._diff_count}: " + ... + >>> print(DeepDiff(t1, t2).pretty(prefix=callback)) + Diff #1: Item root[3] added to set. + Diff #2: Item root[4] removed from set. + Diff #3: Item root[1] removed from set. Text view vs. Tree view vs. vs. pretty() method diff --git a/tests/test_serialization.py b/tests/test_serialization.py index facda24..d578e53 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -330,6 +330,49 @@ def test_pretty_form_method(self, expected, verbose_level): result = ddiff.pretty() assert result == expected + @pytest.mark.parametrize("expected, verbose_level", + ( + ('\t\tItem root[5] added to dictionary.' + '\n\t\tItem root[3] removed from dictionary.' + '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' + '\n\t\tValue of root[4] changed from 4 to 5.', 0), + ('\t\tItem root[5] (5) added to dictionary.' + '\n\t\tItem root[3] (3) removed from dictionary.' + '\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".' + '\n\t\tValue of root[4] changed from 4 to 5.', 2), + ), ids=("verbose=0", "verbose=2") + ) + def test_pretty_form_method_prefixed_simple(self, expected, verbose_level): + t1 = {2: 2, 3: 3, 4: 4} + t2 = {2: 'b', 4: 5, 5: 5} + ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) + result = ddiff.pretty(prefix="\t\t") + assert result == expected + + @pytest.mark.parametrize("expected, verbose_level", + ( + ('Diff #1: Item root[5] added to dictionary.' + '\nDiff #2: Item root[3] removed from dictionary.' + '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' + '\nDiff #4: Value of root[4] changed from 4 to 5.', 0), + ('Diff #1: Item root[5] (5) added to dictionary.' + '\nDiff #2: Item root[3] (3) removed from dictionary.' + '\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".' + '\nDiff #4: Value of root[4] changed from 4 to 5.', 2), + ), ids=("verbose=0", "verbose=2") + ) + def test_pretty_form_method_prefixed_callback(self, expected, verbose_level): + def prefix_callback(**kwargs): + """Helper function using a hidden variable on the diff that tracks which count prints next""" + kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0) + return f"Diff #{kwargs['diff']._diff_count}: " + + t1 = {2: 2, 3: 3, 4: 4} + t2 = {2: 'b', 4: 5, 5: 5} + ddiff = DeepDiff(t1, t2, verbose_level=verbose_level) + result = ddiff.pretty(prefix=prefix_callback) + assert result == expected + @pytest.mark.parametrize('test_num, value, func_to_convert_back', [ (1, {'10': None}, None), (2, {"type_changes": {"root": {"old_type": None, "new_type": list, "new_value": ["你好", 2, 3, 5]}}}, None), From 32df472cdb4c5c5eba6d8bfe8e6f1429649f6460 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sun, 20 Oct 2024 02:55:11 +0300 Subject: [PATCH 08/31] DeepHash: check numpy booleans like native booleans Fixes #494 --- deepdiff/deephash.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deepdiff/deephash.py b/deepdiff/deephash.py index 32fee9c..7c2e2b4 100644 --- a/deepdiff/deephash.py +++ b/deepdiff/deephash.py @@ -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__) @@ -492,7 +497,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): From cee3d41868a9c973c48471f020f63380c271fad0 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sun, 20 Oct 2024 20:05:48 +0300 Subject: [PATCH 09/31] TestDeepHash: test numpy booleans --- tests/test_hash.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_hash.py b/tests/test_hash.py index 5263757..22a86e2 100755 --- a/tests/test_hash.py +++ b/tests/test_hash.py @@ -187,6 +187,12 @@ def test_re(self): a_hash = DeepHash(a)[a] assert not( a_hash is unprocessed) + # https://github.com/seperman/deepdiff/issues/494 + def test_numpy_bool(self): + a = {'b': np.array([True], dtype='bool')} + a_hash = DeepHash(a)[a] + assert not( a_hash is unprocessed) + class TestDeepHashPrep: """DeepHashPrep Tests covering object serialization.""" From 7bb48a13636df3ec9e5a7463a31f8f318ea3e86f Mon Sep 17 00:00:00 2001 From: Joachim Langenbach Date: Sat, 26 Oct 2024 10:31:36 +0200 Subject: [PATCH 10/31] Added missing suffix of tests/test_diff_include_paths_root.py --- tests/{test_diff_include_paths => test_diff_include_paths.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_diff_include_paths => test_diff_include_paths.py} (100%) diff --git a/tests/test_diff_include_paths b/tests/test_diff_include_paths.py similarity index 100% rename from tests/test_diff_include_paths rename to tests/test_diff_include_paths.py From 916f02f6a10f3338219fe3d9b1ae9658ea74c5ce Mon Sep 17 00:00:00 2001 From: Joachim Langenbach Date: Sat, 26 Oct 2024 10:32:33 +0200 Subject: [PATCH 11/31] Added tests for wrong diff result with include_paths and changed number of attributes in dict --- tests/test_diff_include_paths_count.py | 160 +++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 tests/test_diff_include_paths_count.py diff --git a/tests/test_diff_include_paths_count.py b/tests/test_diff_include_paths_count.py new file mode 100644 index 0000000..ccb195c --- /dev/null +++ b/tests/test_diff_include_paths_count.py @@ -0,0 +1,160 @@ +import pytest +from deepdiff import DeepDiff + +@pytest.mark.parametrize( + "data, result", + [ + ( + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + }, + {} + ), + ( + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath New', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + }, + {"values_changed": {"root['sub_path']['name']": {"old_value": "Testname Subpath old", "new_value": "Testname Subpath New"}}} + ), + ( + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + 'old_attr': 'old attr value', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath New', + 'new_sub_path_attr': 'new sub path attr value', + }, + }, + "include_paths": "root['sub_path']['name']", + }, + {} + ), + ( + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + }, + {} + ), + ( + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + }, + {} + ), + ( + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + }, + {} + ), + ( + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'blu', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + }, + {} + ), + ] +) +def test_diff_include_paths_root(data, result): + diff = DeepDiff(data["old"], data["new"], include_paths=data["include_paths"]) + assert diff == result From fc8baaafc7077ca86c5d258e3aa1bb503b335db2 Mon Sep 17 00:00:00 2001 From: Joachim Langenbach Date: Sat, 26 Oct 2024 12:08:24 +0200 Subject: [PATCH 12/31] Fixed include_paths fault, if only certain keys of a path are included --- deepdiff/diff.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4dfec50..61284af 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -510,6 +510,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. @@ -570,11 +596,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) From 2d61bb1767e27eb80bd5b939f7de5d333e8613d7 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Thu, 14 Nov 2024 00:03:53 -0800 Subject: [PATCH 13/31] updating dev dependencies. Adding tests for include_paths --- requirements-cli.txt | 2 +- requirements-dev.txt | 30 ++-- tests/test_command.py | 2 +- tests/test_diff_include_paths | 81 --------- tests/test_diff_include_paths.py | 282 +++++++++++++++++++++++++++++++ tests/test_diff_text.py | 53 ++++++ 6 files changed, 352 insertions(+), 98 deletions(-) delete mode 100644 tests/test_diff_include_paths create mode 100644 tests/test_diff_include_paths.py diff --git a/requirements-cli.txt b/requirements-cli.txt index 0ba0c7e..5f1275e 100644 --- a/requirements-cli.txt +++ b/requirements-cli.txt @@ -1,2 +1,2 @@ click==8.1.7 -pyyaml==6.0.1 +pyyaml==6.0.2 diff --git a/requirements-dev.txt b/requirements-dev.txt index 5241e2b..e91956f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,23 +1,23 @@ -r requirements.txt -r requirements-cli.txt bump2version==1.0.1 -jsonpickle==3.2.1 -coverage==7.5.3 +jsonpickle==4.0.0 +coverage==7.6.4 ipdb==0.13.13 -numpy==2.0.0 -pytest==8.2.2 -pytest-cov==5.0.0 +numpy==2.1.3 +pytest==8.3.3 +pytest-cov==6.0.0 python-dotenv==1.0.1 Sphinx==6.2.1 # We use the html style that is not supported in Sphinx 7 anymore. sphinx-sitemap==2.6.0 -sphinxemoji==0.2.0 -flake8==7.1.0 +sphinxemoji==0.3.1 +flake8==7.1.1 python-dateutil==2.9.0.post0 -orjson==3.10.5 -wheel==0.43.0 -tomli==2.0.1 -tomli-w==1.0.0 -pydantic==2.7.4 -pytest-benchmark==4.0.0 -pandas==2.2.2 -polars==1.0.0 +orjson==3.10.11 +wheel==0.45.0 +tomli==2.1.0 +tomli-w==1.1.0 +pydantic==2.9.2 +pytest-benchmark==5.1.0 +pandas==2.2.3 +polars==1.13.1 diff --git a/tests/test_command.py b/tests/test_command.py index bc97e01..933cb6a 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -12,7 +12,7 @@ class TestCommands: @pytest.mark.parametrize('name1, name2, expected_in_stdout, expected_exit_code', [ ('t1.json', 't2.json', """dictionary_item_added": [\n "root[0][\'key3\']""", 0), - ('t1_corrupt.json', 't2.json', "Expecting property name enclosed in double quotes", 1), + ('t1_corrupt.json', 't2.json', "Error when loading t1: Illegal trailing comma before end of object: line 3 column 21 (char 45)\n", 1), ('t1.json', 't2_json.csv', '"old_value": "value2"', 0), ('t2_json.csv', 't1.json', '"old_value": "value3"', 0), ('t1.csv', 't2.csv', '"new_value": "James"', 0), diff --git a/tests/test_diff_include_paths b/tests/test_diff_include_paths deleted file mode 100644 index 9dace5c..0000000 --- a/tests/test_diff_include_paths +++ /dev/null @@ -1,81 +0,0 @@ -import pytest -from deepdiff import DeepDiff - -t1 = { - "foo": { - "bar": { - "veg": "potato", - "fruit": "apple" - } - }, - "ingredients": [ - { - "lunch": [ - "bread", - "cheese" - ] - }, - { - "dinner": [ - "soup", - "meat" - ] - } - ] -} -t2 = { - "foo": { - "bar": { - "veg": "potato", - "fruit": "peach" - } - }, - "ingredients": [ - { - "lunch": [ - "bread", - "cheese" - ] - }, - { - "dinner": [ - "soup", - "meat" - ] - } - ] -} - - -class TestDeepDiffIncludePaths: - - @staticmethod - def deep_diff(dict1, dict2, include_paths): - diff = DeepDiff(dict1, dict2, include_paths=include_paths) - print(diff) - return diff - - def test_include_paths_root_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, 'foo') - assert expected == actual - - def test_include_paths_root_pos(self): - expected = {} - actual = self.deep_diff(t1, t2, 'ingredients') - assert expected == actual - - def test_include_paths_nest00_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, "root['foo']['bar']") - assert expected == actual - - def test_include_paths_nest01_neg(self): - expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} - actual = self.deep_diff(t1, t2, "root['foo']['bar']['fruit']") - assert expected == actual - - def test_include_paths_nest_pos(self): - expected = {} - actual = self.deep_diff(t1, t2, "root['foo']['bar']['veg']") - assert expected == actual diff --git a/tests/test_diff_include_paths.py b/tests/test_diff_include_paths.py new file mode 100644 index 0000000..8e6c246 --- /dev/null +++ b/tests/test_diff_include_paths.py @@ -0,0 +1,282 @@ +import pytest +from deepdiff import DeepDiff + +t1 = { + "foo": { + "bar": { + "veg": "potato", + "fruit": "apple" + } + }, + "ingredients": [ + { + "lunch": [ + "bread", + "cheese" + ] + }, + { + "dinner": [ + "soup", + "meat" + ] + } + ] +} +t2 = { + "foo": { + "bar": { + "veg": "potato", + "fruit": "peach" + } + }, + "ingredients": [ + { + "lunch": [ + "bread", + "cheese" + ] + }, + { + "dinner": [ + "soup", + "meat" + ] + } + ] +} + + +class TestDeepDiffIncludePaths: + + @staticmethod + def deep_diff(dict1, dict2, include_paths): + diff = DeepDiff(dict1, dict2, include_paths=include_paths) + print(diff) + return diff + + def test_include_paths_root_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, 'foo') + assert expected == actual + + def test_include_paths_root_pos(self): + expected = {} + actual = self.deep_diff(t1, t2, 'ingredients') + assert expected == actual + + def test_include_paths_nest00_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, "root['foo']['bar']") + assert expected == actual + + def test_include_paths_nest01_neg(self): + expected = {'values_changed': {"root['foo']['bar']['fruit']": {'new_value': 'peach', 'old_value': 'apple'}}} + actual = self.deep_diff(t1, t2, "root['foo']['bar']['fruit']") + assert expected == actual + + def test_include_paths_nest_pos(self): + expected = {} + actual = self.deep_diff(t1, t2, "root['foo']['bar']['veg']") + assert expected == actual + + @pytest.mark.parametrize( + "test_num, data", + [ + ( + 1, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}}}, + "expected_result2": {}, + }, + ), + ( + 2, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath New', + 'desciption': 'Desc Subpath old', + }, + }, + "include_paths": "root['sub_path']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['name']": {'new_value': 'Testname Subpath New', 'old_value': 'Testname Subpath old'}}}, + "expected_result2": {"values_changed": {"root['sub_path']['name']": {"old_value": "Testname Subpath old", "new_value": "Testname Subpath New"}}}, + }, + ), + ( + 3, # test_num + { + "old": { + 'name': 'Testname Old', + 'desciption': 'Desc Old', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath old', + 'old_attr': 'old attr value', + }, + }, + "new": { + 'name': 'Testname New', + 'desciption': 'Desc New', + 'new_attribute': 'New Value', + 'sub_path': { + 'name': 'Testname Subpath old', + 'desciption': 'Desc Subpath New', + 'new_sub_path_attr': 'new sub path attr value', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'dictionary_item_added': ["root['new_attribute']", "root['sub_path']['new_sub_path_attr']"], 'dictionary_item_removed': ["root['sub_path']['old_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname New', 'old_value': 'Testname Old'}, "root['desciption']": {'new_value': 'Desc New', 'old_value': 'Desc Old'}, "root['sub_path']['desciption']": {'new_value': 'Desc Subpath New', 'old_value': 'Desc Subpath old'}}}, + "expected_result2": {}, + }, + ), + ( + 4, # test_num + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'dictionary_item_added': ["root['sub_path']['added_attr']"], 'dictionary_item_removed': ["root['sub_path']['removed_attr']"], 'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}}}, + "expected_result2": {}, + }, + ), + ( + 5, # test_num + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + "expected_result1": {'dictionary_item_added': ["root['added_attr']"], 'dictionary_item_removed': ["root['removed_attr']"]}, + "expected_result2": {}, + }, + ), + ( + 6, # test_num + { + "old": { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'revemod attr value', + }, + "new": { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + "include_paths": "root['name']", + "expected_result1": {'values_changed': {'root': {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'revemod attr value'}}}}, + "expected_result2": {}, + }, + ), + ( + 7, # test_num + { + "old": { + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'blu', + }, + }, + "new": { + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'Testname', + }, + }, + "include_paths": "root['sub_path']['name']", + "expected_result1": {'values_changed': {"root['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, + "expected_result2": {}, + }, + ), + ( + 8, # test_num + { + "old": [{ + 'name': 'Testname old', + 'desciption': 'Desc old', + 'new_attribute': 'old Value', + 'sub_path': { + 'name': 'Testname', + 'removed_attr': 'revemod attr value', + 'removed_attr_2': 'blu', + }, + }], + "new": [{ + 'name': 'Testname new', + 'desciption': 'Desc new', + 'new_attribute': 'new Value', + 'sub_path': { + 'added_attr': 'Added Attr Value', + 'name': 'New Testname', + }, + }], + "include_paths": "root[0]['sub_path']['name']", + "expected_result1": {'values_changed': {"root[0]['name']": {'new_value': 'Testname new', 'old_value': 'Testname old'}, "root[0]['desciption']": {'new_value': 'Desc new', 'old_value': 'Desc old'}, "root[0]['new_attribute']": {'new_value': 'new Value', 'old_value': 'old Value'}, "root[0]['sub_path']": {'new_value': {'added_attr': 'Added Attr Value', 'name': 'New Testname'}, 'old_value': {'name': 'Testname', 'removed_attr': 'revemod attr value', 'removed_attr_2': 'blu'}}}}, + "expected_result2": {'values_changed': {"root[0]['sub_path']['name']": {'new_value': 'New Testname', 'old_value': 'Testname'}}}, + }, + ), + ] + ) + def test_diff_include_paths_root(self, test_num, data): + diff1 = DeepDiff(data["old"], data["new"]) + diff2 = DeepDiff(data["old"], data["new"], include_paths=data["include_paths"]) + assert data['expected_result1'] == diff1, f"test_diff_include_paths_root test_num #{test_num} failed." + assert data['expected_result2'] == diff2, f"test_diff_include_paths_root test_num #{test_num} failed." diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index ec6f66b..e78c8eb 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -1570,6 +1570,59 @@ def test_include_path4_nested(self): } } == ddiff + def test_include_path5(self): + diff = DeepDiff( + { + 'name': 'Testname', + 'code': 'bla', + 'noneCode': 'blu', + }, { + 'uid': '12345', + 'name': 'Testname2', + }, + ) + + diff2 = DeepDiff( + { + 'name': 'Testname', + 'code': 'bla', + 'noneCode': 'blu', + }, { + 'uid': '12345', + 'name': 'Testname2', + }, + include_paths = "root['name']" + ) + expected = {'values_changed': {'root': {'new_value': {'uid': '12345', 'name': 'Testname2'}, 'old_value': {'name': 'Testname', 'code': 'bla', 'noneCode': 'blu'}}}} + expected2 = {'values_changed': {"root['name']": {'new_value': 'Testname2', 'old_value': 'Testname'}}} + + assert expected == diff + assert expected2 == diff2 + + def test_include_path6(self): + t1 = [1, 2, 3, [4, 5, {6: 7}]] + t2 = [1, 2, 3, [4, 5, {6: 1000}]] + diff = DeepDiff( + t1, + t2, + ) + + diff2 = DeepDiff( + t1, + t2, + include_paths = "root[3]" + ) + + diff3 = DeepDiff( + t1, + t2, + include_paths = "root[4]" + ) + expected = {'values_changed': {'root[3][2][6]': {'new_value': 1000, 'old_value': 7}}} + assert expected == diff + assert diff == diff2 + assert not diff3 + def test_skip_path4(self): t1 = { "for life": "vegan", From f6c7bcb1ab65b65a8d0ff49f6349099b99e63c8a Mon Sep 17 00:00:00 2001 From: Mate Valko <3168272+vmatt@users.noreply.github.com> Date: Mon, 18 Nov 2024 11:22:14 +0100 Subject: [PATCH 14/31] Only lower if clean_key is instance of str --- deepdiff/diff.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4dfec50..da2ea8e 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -530,7 +530,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' From 360c2f27c62a267baa4c82393730451029641b5c Mon Sep 17 00:00:00 2001 From: Juergen Skrotzky Date: Mon, 18 Nov 2024 14:30:15 +0100 Subject: [PATCH 15/31] Add empty py.typed --- deepdiff/py.typed | 0 setup.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 deepdiff/py.typed diff --git a/deepdiff/py.typed b/deepdiff/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py index e18d83f..5ae81bf 100755 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ def get_reqs(filename): author_email='sep@zepworks.com', license='MIT', packages=['deepdiff'], + package_data={"deepdiff": ["py.typed"]}, zip_safe=True, include_package_data=True, long_description=long_description, From d1c8f90b6df3a3906b65e4fa7f05fdd5b4d1f39a Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 11:23:39 -0800 Subject: [PATCH 16/31] adding 2 more tests --- tests/test_delta.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_delta.py b/tests/test_delta.py index 81a0578..fe328b6 100644 --- a/tests/test_delta.py +++ b/tests/test_delta.py @@ -595,6 +595,7 @@ def compare_func(item1, item2, level=None): delta = Delta(flat_rows_list=flat_rows_list, always_include_values=True, bidirectional=True, raise_errors=True) + flat_rows_list_again = delta.to_flat_rows() # if the flat_rows_list is (unexpectedly) mutated, it will be missing the list index number on the path value. old_mutated_list_missing_indexes_on_path = [FlatDeltaRow(path=['individualNames'], value={'firstName': 'Johnny', @@ -620,6 +621,7 @@ def compare_func(item1, item2, level=None): # Verify that our fix in the delta constructor worked... assert flat_rows_list != old_mutated_list_missing_indexes_on_path assert flat_rows_list == preserved_flat_dict_list + assert flat_rows_list == flat_rows_list_again picklalbe_obj_without_item = PicklableClass(11) @@ -874,6 +876,13 @@ def compare_func(item1, item2, level=None): 'to_delta_kwargs': {'directed': True}, 'expected_delta_dict': {'values_changed': {'root["a\'][\'b\'][\'c"]': {'new_value': 2}}} }, + 'delta_case21_empty_list_add': { + 't1': {'car_model': [], 'car_model_version_id': 0}, + 't2': {'car_model': ['Super Duty F-250'], 'car_model_version_id': 1}, + 'deepdiff_kwargs': {}, + 'to_delta_kwargs': {'directed': True}, + 'expected_delta_dict': {'iterable_item_added': {"root['car_model'][0]": 'Super Duty F-250'}, 'values_changed': {"root['car_model_version_id']": {'new_value': 1}}}, + }, } @@ -2469,6 +2478,33 @@ def test_delta_flat_rows(self): delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True, force=True) assert t1 + delta2 == t2 + def test_delta_bool(self): + flat_rows_list = [FlatDeltaRow(path=['dollar_to_cent'], action='values_changed', value=False, old_value=True, type=bool, old_type=bool)] + value = {'dollar_to_cent': False} + delta = Delta(flat_rows_list=flat_rows_list, bidirectional=True, force=True) + assert {'dollar_to_cent': True} == value - delta + + def test_detla_add_to_empty_iterable_and_flatten(self): + t1 = {'models': [], 'version_id': 0} + t2 = {'models': ['Super Duty F-250'], 'version_id': 1} + t3 = {'models': ['Super Duty F-250', 'Focus'], 'version_id': 1} + diff = DeepDiff(t1, t2, verbose_level=2) + delta = Delta(diff, bidirectional=True) + assert t1 + delta == t2 + flat_rows = delta.to_flat_rows() + delta2 = Delta(flat_rows_list=flat_rows, bidirectional=True) # , force=True + assert t1 + delta2 == t2 + assert t2 - delta2 == t1 + + diff3 = DeepDiff(t2, t3) + delta3 = Delta(diff3, bidirectional=True) + flat_dicts3 = delta3.to_flat_dicts() + + delta3_again = Delta(flat_dict_list=flat_dicts3, bidirectional=True) + assert t2 + delta3_again == t3 + assert t3 - delta3_again == t2 + + def test_flat_dict_and_deeply_nested_dict(self): beforeImage = [ { From fe9fa861b2766a157ccf2b6b978b6b93565a59c0 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 11:29:21 -0800 Subject: [PATCH 17/31] adding python 3.13 --- .github/workflows/main.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1d7584c..5a69284 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -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 @@ -44,34 +44,34 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - name: Upgrade setuptools - if: matrix.python-version == 3.12 + if: matrix.python-version == 3.13 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: From 31d727547592d2ab54380cf95a926a32b59bffda Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 11:31:05 -0800 Subject: [PATCH 18/31] Update CHANGELOG.md Co-authored-by: Mathis Chenuet <9201969+artemisart@users.noreply.github.com> --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12da1c2..61c4013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,14 @@ # DeepDiff Change log +- v8-1-0 + - Fixed slots-classes comparison. + - Added `prefix` option to `pretty()` + - Relax `orderly-set` dependency. + - Fixes hashing of numpy boolean values. + - v8-0-1 - Bugfix. Numpy should be optional. - - Added `prefix` option to `pretty()` - v8-0-0 From 6d819f077ef2e97698c734da0cb402682b0662df Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 11:38:43 -0800 Subject: [PATCH 19/31] fixing the tests for old pythons --- requirements-dev.txt | 2 +- requirements-dev3.8.txt | 2 +- tests/test_command.py | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e91956f..3a0f083 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ sphinx-sitemap==2.6.0 sphinxemoji==0.3.1 flake8==7.1.1 python-dateutil==2.9.0.post0 -orjson==3.10.11 +orjson==3.10.12 wheel==0.45.0 tomli==2.1.0 tomli-w==1.1.0 diff --git a/requirements-dev3.8.txt b/requirements-dev3.8.txt index 532e141..b39b7fe 100644 --- a/requirements-dev3.8.txt +++ b/requirements-dev3.8.txt @@ -14,7 +14,7 @@ sphinx-sitemap==2.6.0 sphinxemoji==0.2.0 flake8==7.1.0 python-dateutil==2.9.0.post0 -orjson==3.10.5 +orjson==3.10.12 wheel==0.43.0 tomli==2.0.1 tomli-w==1.0.0 diff --git a/tests/test_command.py b/tests/test_command.py index 933cb6a..fa671cc 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -12,7 +12,7 @@ class TestCommands: @pytest.mark.parametrize('name1, name2, expected_in_stdout, expected_exit_code', [ ('t1.json', 't2.json', """dictionary_item_added": [\n "root[0][\'key3\']""", 0), - ('t1_corrupt.json', 't2.json', "Error when loading t1: Illegal trailing comma before end of object: line 3 column 21 (char 45)\n", 1), + ('t1_corrupt.json', 't2.json', "Error when loading t1:", 1), ('t1.json', 't2_json.csv', '"old_value": "value2"', 0), ('t2_json.csv', 't1.json', '"old_value": "value3"', 0), ('t1.csv', 't2.csv', '"new_value": "James"', 0), @@ -23,6 +23,7 @@ class TestCommands: def test_diff_command(self, name1, name2, expected_in_stdout, expected_exit_code): t1 = os.path.join(FIXTURES_DIR, name1) t2 = os.path.join(FIXTURES_DIR, name2) + runner = CliRunner() result = runner.invoke(diff, [t1, t2]) assert result.exit_code == expected_exit_code, f"test_diff_command failed for {name1}, {name2}" From 2f290fec4b9cd303e64df1270c3d2e995649d334 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 11:53:28 -0800 Subject: [PATCH 20/31] upgrading dependencies --- requirements-dev.txt | 14 +++++++------- requirements.txt | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3a0f083..fce48a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,22 +2,22 @@ -r requirements-cli.txt bump2version==1.0.1 jsonpickle==4.0.0 -coverage==7.6.4 +coverage==7.6.9 ipdb==0.13.13 numpy==2.1.3 -pytest==8.3.3 +pytest==8.3.4 pytest-cov==6.0.0 python-dotenv==1.0.1 -Sphinx==6.2.1 # We use the html style that is not supported in Sphinx 7 anymore. +Sphinx==6.2.1 # We use the html style that is not supported in Sphinx 7 anymore. sphinx-sitemap==2.6.0 sphinxemoji==0.3.1 flake8==7.1.1 python-dateutil==2.9.0.post0 orjson==3.10.12 -wheel==0.45.0 -tomli==2.1.0 +wheel==0.45.1 +tomli==2.2.1 tomli-w==1.1.0 -pydantic==2.9.2 +pydantic==2.10.3 pytest-benchmark==5.1.0 pandas==2.2.3 -polars==1.13.1 +polars==1.16.0 diff --git a/requirements.txt b/requirements.txt index 53ac539..8270bf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -orderly-set>=5.2.3,<6 \ No newline at end of file +orderly-set>=5.2.3,<6 From 151dbddece66f85d467543864ac98dbe59a9ff7b Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 12:17:25 -0800 Subject: [PATCH 21/31] only limit to 3.12 to check faster for the issue --- .github/workflows/main.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5a69284..6d38ac9 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.13"] architecture: ["x64"] steps: - uses: actions/checkout@v2 @@ -44,7 +44,7 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - name: Upgrade setuptools - if: matrix.python-version == 3.13 + if: matrix.python-version => 3.12 run: | # workaround for 3.13, SEE: https://github.com/pypa/setuptools/issues/3661#issuecomment-1813845177 pip install --upgrade setuptools From d7e2a94b05cb89876fe1a35f263c3da75d21491b Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Fri, 6 Dec 2024 12:19:15 -0800 Subject: [PATCH 22/31] somehow git actions didn't work. reverting. --- .github/workflows/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 6d38ac9..4bbcd75 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.13"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] architecture: ["x64"] steps: - uses: actions/checkout@v2 From 85adbd2e27bff66ee530ad95a7a6c51f627d4096 Mon Sep 17 00:00:00 2001 From: Mate Valko Date: Sat, 7 Dec 2024 23:21:27 +0100 Subject: [PATCH 23/31] add tests for group_by None cases --- tests/test_diff_text.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index ec6f66b..b41384d 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -2147,3 +2147,25 @@ class MyDataClass: diff = DeepDiff(t1, t2, exclude_regex_paths=["any"]) assert {'values_changed': {'root[MyDataClass(val=2,val2=4)]': {'new_value': 10, 'old_value': 20}}} == diff + + + def test_group_by_with_none_key_and_ignore_case(self): + """Test that group_by works with None keys when ignore_string_case is True""" + dict1 = [{'txt_field': 'FULL_NONE', 'group_id': None}, {'txt_field': 'FULL', 'group_id': 'a'}] + dict2 = [{'txt_field': 'PARTIAL_NONE', 'group_id': None}, {'txt_field': 'PARTIAL', 'group_id': 'a'}] + + diff = DeepDiff( + dict1, + dict2, + ignore_order=True, + group_by='group_id', + ignore_string_case=True + ) + + expected = {'values_changed': {"root[None]['txt_field']": + {'new_value': 'partial_none', 'old_value': 'full_none'}, + "root['a']['txt_field']": + {'new_value': 'partial', 'old_value': 'full'} + } + } + assert expected == diff From 324aad307f1af7e050ef8f66887e43a3c66f3e04 Mon Sep 17 00:00:00 2001 From: Sherjeel Shabih Date: Mon, 9 Dec 2024 19:19:43 +0100 Subject: [PATCH 24/31] Fixes __len__ of TreeResult when only comparing un-nested types --- deepdiff/model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepdiff/model.py b/deepdiff/model.py index 2373195..9304976 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -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] @@ -88,7 +88,7 @@ def __getitem__(self, item): return self.get(item) def __len__(self): - return sum([len(i) for i in self.values() if isinstance(i, SetOrdered)]) + return sum([len(i) for i in self.values() if isinstance(i, SetOrdered)]) + len([i for i in self.values() if isinstance(i, int)]) class TextResult(ResultDict): From 051c6d808d8e59cc5428d53371c3e2ca9a84d0b4 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Sat, 14 Dec 2024 13:08:47 -0800 Subject: [PATCH 25/31] better support for Pydantic models. Ignore model_fields_set when comparing pydantic objects --- deepdiff/deephash.py | 8 ++++++-- deepdiff/diff.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/deepdiff/deephash.py b/deepdiff/deephash.py index 7c2e2b4..1f293bd 100644 --- a/deepdiff/deephash.py +++ b/deepdiff/deephash.py @@ -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: @@ -331,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__) @@ -562,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) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 5ec9ae1..27b4838 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -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 @@ -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) @@ -1678,7 +1684,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) From 5120230f8d90b1be8336aaeb4a68df80a68f07f0 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Sat, 14 Dec 2024 13:13:08 -0800 Subject: [PATCH 26/31] slight optimization of TreeResult len --- deepdiff/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/deepdiff/model.py b/deepdiff/model.py index 9304976..298824a 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -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)]) + len([i for i in self.values() if isinstance(i, int)]) + 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): From f1d87e98d2b4be89b221fce9d50233de60e04a60 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Sat, 14 Dec 2024 23:32:53 -0800 Subject: [PATCH 27/31] fixes #509 --- deepdiff/diff.py | 10 ++++++++-- tests/test_diff_text.py | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 056b040..45f0328 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -616,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 diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index 012115e..4de67b2 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -1533,6 +1533,10 @@ def test_skip_path2_reverse(self): ddiff = DeepDiff(t2, t1, exclude_paths={"root['ingredients']"}) assert {} == ddiff + def test_exclude_path_when_prefix_of_exclude_path_matches1(self): + diff = DeepDiff({}, {'foo': '', 'bar': ''}, exclude_paths=['foo', 'bar']) + assert not diff + def test_include_path3(self): t1 = { "for life": "vegan", From 42fd42ddbeac7a49fe89a3f09dd6f59fbb1a1e55 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Sat, 14 Dec 2024 23:51:55 -0800 Subject: [PATCH 28/31] fixes to_json() method chokes on some standard json.dumps() such as sort_keys #490 --- deepdiff/serialization.py | 30 ++++++++++++++++++++++++++---- tests/test_serialization.py | 8 ++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index e350b3c..1ad12a5 100644 --- a/deepdiff/serialization.py +++ b/deepdiff/serialization.py @@ -179,7 +179,7 @@ def from_json_pickle(cls, value): else: logger.error('jsonpickle library needs to be installed in order to run from_json_pickle') # pragma: no cover. Json pickle is getting deprecated. - def to_json(self, default_mapping=None, **kwargs): + def to_json(self, default_mapping: dict | None=None, force_use_builtin_json=False, **kwargs): """ Dump json of the text view. **Parameters** @@ -190,6 +190,11 @@ def to_json(self, default_mapping=None, **kwargs): If you have a certain object type that the json serializer can not serialize it, please pass the appropriate type conversion through this dictionary. + force_use_builtin_json: Boolean, default = False + When True, we use Python's builtin Json library for serialization, + even if Orjson is installed. + + kwargs: Any other kwargs you pass will be passed on to Python's json.dumps() **Example** @@ -212,7 +217,12 @@ def to_json(self, default_mapping=None, **kwargs): '{"type_changes": {"root": {"old_type": "A", "new_type": "B", "old_value": "obj A", "new_value": "obj B"}}}' """ dic = self.to_dict(view_override=TEXT_VIEW) - return json_dumps(dic, default_mapping=default_mapping, **kwargs) + return json_dumps( + dic, + default_mapping=default_mapping, + force_use_builtin_json=force_use_builtin_json, + **kwargs, + ) def to_dict(self, view_override=None): """ @@ -637,14 +647,26 @@ def object_hook(self, obj): return obj -def json_dumps(item, default_mapping=None, **kwargs): +def json_dumps(item, default_mapping=None, force_use_builtin_json: bool=False, **kwargs): """ Dump json with extra details that are not normally json serializable + + parameters + ---------- + + force_use_builtin_json: Boolean, default = False + When True, we use Python's builtin Json library for serialization, + even if Orjson is installed. """ - if orjson: + if orjson and not force_use_builtin_json: indent = kwargs.pop('indent', None) if indent: kwargs['option'] = orjson.OPT_INDENT_2 + if 'sort_keys' in kwargs: + raise TypeError( + "orjson does not accept the sort_keys parameter. " + "If you need to pass sort_keys, set force_use_builtin_json=True " + "to use Python's built-in json library instead of orjson.") return orjson.dumps( item, default=json_convertor_default(default_mapping=default_mapping), diff --git a/tests/test_serialization.py b/tests/test_serialization.py index d578e53..3c50683 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -47,6 +47,14 @@ def test_serialization_text(self): jsoned = ddiff.to_json() assert "world" in jsoned + def test_serialization_text_force_builtin_json(self): + ddiff = DeepDiff(t1, t2) + with pytest.raises(TypeError) as excinfo: + jsoned = ddiff.to_json(sort_keys=True) + assert str(excinfo.value).startswith("orjson does not accept the sort_keys parameter.") + jsoned = ddiff.to_json(sort_keys=True, force_use_builtin_json=True) + assert "world" in jsoned + def test_deserialization(self): ddiff = DeepDiff(t1, t2) jsoned = ddiff.to_json_pickle() From c464e04d987c6abb5b92b0c5d0cd56d3fbfdf29e Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Sun, 15 Dec 2024 00:18:41 -0800 Subject: [PATCH 29/31] fixes accessing the affected_root_keys property on the diff object returned by DeepDiff fails when one of the dicts is empty #508 --- deepdiff/diff.py | 8 ++++++-- deepdiff/model.py | 4 +++- tests/test_diff_text.py | 25 +++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 45f0328..a6fe06b 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -1846,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 diff --git a/deepdiff/model.py b/deepdiff/model.py index 298824a..f5e5a4d 100644 --- a/deepdiff/model.py +++ b/deepdiff/model.py @@ -665,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'): """ diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index 4de67b2..63df30a 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -807,6 +807,24 @@ class ClassB: result = {'attribute_removed': ['root.y']} assert result == ddiff + def test_custom_objects_slot_in_group_change(self): + class ClassA: + __slots__ = ('x', 'y') + + def __init__(self, x, y): + self.x = x + self.y = y + + class ClassB(ClassA): + pass + + t1 = ClassA(1, 1) + t2 = ClassB(1, 1) + ddiff = DeepDiff(t1, t2, ignore_type_in_groups=[(ClassA, ClassB)]) + result = {} + assert result == ddiff + + def test_custom_class_changes_none_when_ignore_type(self): ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)]) result = { @@ -2226,3 +2244,10 @@ def test_group_by_with_none_key_and_ignore_case(self): } } assert expected == diff + + def test_affected_root_keys_when_dict_empty(self): + diff = DeepDiff({}, {1:1, 2:2}, threshold_to_diff_deeper=0) + assert [1, 2] == diff.affected_root_keys + + diff2 = DeepDiff({}, {1:1, 2:2}) + assert [] == diff2.affected_root_keys From 737bb5ae1e16b529522cc7f97f2a501c66971618 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Mon, 16 Dec 2024 15:21:17 -0800 Subject: [PATCH 30/31] updating docs --- AUTHORS.md | 11 +++++++++-- CHANGELOG.md | 17 +++++++++++++++-- README.md | 19 +++++++++++++++++++ deepdiff/serialization.py | 5 ++++- docs/authors.rst | 18 ++++++++++++++++++ docs/index.rst | 26 ++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 5 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 79d9edb..1f8fe5c 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,5 +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. -- [Aaron D. Marasco](https://github.com/AaronDMarasco) added `prefix` option to `pretty()` +- 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c4013..9273ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,23 @@ - v8-1-0 - - Fixed slots-classes comparison. + - Removing deprecated lines from setup.py - Added `prefix` option to `pretty()` - - Relax `orderly-set` dependency. - 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. diff --git a/README.md b/README.md index 22d86dc..5636f17 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index 1ad12a5..13c1da6 100644 --- a/deepdiff/serialization.py +++ b/deepdiff/serialization.py @@ -44,6 +44,7 @@ from copy import deepcopy, copy from functools import partial from collections.abc import Mapping +from typing import Callable from deepdiff.helper import ( strings, get_type, @@ -306,11 +307,13 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True, always_ return deepcopy(dict(result)) - def pretty(self, prefix=None): + def pretty(self, prefix: str | Callable=None): """ The pretty human readable string output for the diff object regardless of what view was used to generate the diff. + prefix can be a callable or a string or None. + Example: >>> t1={1,2,4} >>> t2={2,3} diff --git a/docs/authors.rst b/docs/authors.rst index 1ca60ae..1226d62 100644 --- a/docs/authors.rst +++ b/docs/authors.rst @@ -93,6 +93,24 @@ Authors in order of the timeline of their contributions: moved items when iterable_compare_func is used. - `Florian Finkernagel `__ for pandas and polars support. +- Mathis Chenuet `artemisart `__ for + fixing slots classes comparison and PR review. +- Sherjeel Shabih `sherjeelshabih `__ + for fixing the issue where the key deep_distance is not returned when + both compared items are equal #510 +- `Juergen Skrotzky `__ for adding + empty ``py.typed`` +- `Mate Valko `__ for fixing the issue so we + lower only if clean_key is instance of str via #504 +- `jlaba `__ for fixing #493 include_paths, + when only certain keys are included via #499 +- `Doron Behar `__ for fixing DeepHash + for numpy booleans via #496 +- `Aaron D. Marasco `__ for adding + print() options which allows a user-defined string (or callback + function) to prefix every output when using the pretty() call. +- `David Hotham `__ for relaxing + orderly-set dependency via #486 .. _Sep Dehpour (Seperman): http://www.zepworks.com .. _Victor Hahn Castell: http://hahncastell.de diff --git a/docs/index.rst b/docs/index.rst index dcaafef..bccdc8d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -31,6 +31,32 @@ The DeepDiff library includes the following modules: What Is New *********** +DeepDiff 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 + + DeepDiff 8-0-1 - Bugfix. Numpy should be optional. From d2d38064caa8cbacf87a91000332f77570bb7051 Mon Sep 17 00:00:00 2001 From: Sep Dehpour Date: Mon, 16 Dec 2024 15:25:08 -0800 Subject: [PATCH 31/31] fixing types to be compatible for python 3.8 --- deepdiff/serialization.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index 13c1da6..4119742 100644 --- a/deepdiff/serialization.py +++ b/deepdiff/serialization.py @@ -44,7 +44,7 @@ from copy import deepcopy, copy from functools import partial from collections.abc import Mapping -from typing import Callable +from typing import Callable, Optional, Union from deepdiff.helper import ( strings, get_type, @@ -180,7 +180,7 @@ def from_json_pickle(cls, value): else: logger.error('jsonpickle library needs to be installed in order to run from_json_pickle') # pragma: no cover. Json pickle is getting deprecated. - def to_json(self, default_mapping: dict | None=None, force_use_builtin_json=False, **kwargs): + def to_json(self, default_mapping: Optional[dict]=None, force_use_builtin_json=False, **kwargs): """ Dump json of the text view. **Parameters** @@ -307,7 +307,7 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True, always_ return deepcopy(dict(result)) - def pretty(self, prefix: str | Callable=None): + def pretty(self, prefix: Optional[Union[str, Callable]]=None): """ The pretty human readable string output for the diff object regardless of what view was used to generate the diff.