diff --git a/AUTHORS.md b/AUTHORS.md index cd3db130..79d9edbf 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -63,3 +63,5 @@ 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()` diff --git a/CHANGELOG.md b/CHANGELOG.md index 95cd2c74..12da1c20 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/deephash.py b/deepdiff/deephash.py index 32fee9c3..7c2e2b47 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): diff --git a/deepdiff/diff.py b/deepdiff/diff.py index 4dfec50c..5ec9ae10 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, 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) @@ -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) diff --git a/deepdiff/serialization.py b/deepdiff/serialization.py index 5b4075e2..e350b3cf 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 f50fc9f1..6343590f 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/requirements-cli.txt b/requirements-cli.txt index 0ba0c7e6..5f1275e8 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 5241e2bf..e91956f3 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/requirements.txt b/requirements.txt index 28bbd74e..53ac539e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -orderly-set==5.2.2 +orderly-set>=5.2.3,<6 \ No newline at end of file diff --git a/setup.py b/setup.py index 7db28b65..e18d83fb 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, diff --git a/tests/test_command.py b/tests/test_command.py index bc97e011..933cb6a2 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 9dace5cd..00000000 --- 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 00000000..8e6c2464 --- /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 ec6f66b4..14091eca 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", @@ -1713,7 +1766,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): diff --git a/tests/test_hash.py b/tests/test_hash.py index 52637577..22a86e24 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.""" diff --git a/tests/test_serialization.py b/tests/test_serialization.py index facda246..d578e53a 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),