Repository: seperman/deepdiff

Issue:
deepdiff with include-paths is changing results, if a not considered path changes
Comparing some paths of two dicts with deepdiff the results differ, depending on the presence or absence of other "not included" paths (see also [Stackoverflow question](https://stackoverflow.com/questions/79070493/python-deepdiff-include-paths-different-result-caused-by-not-included-paths)).

```
from deepdiff import DeepDiff
DeepDiff(
    {
        'name': 'Testname',
        'code': 'bla',
        'noneCode': 'blu',
    }, {
        'uid': '12345',
        'name': 'Testname',
    },
    include_paths = "root['name']"
) # expected to be {}, but detects a change

DeepDiff(
    {
        'name': 'Testname',
        'code': 'bla',
    }, {
        'uid': '12345',
        'name': 'Testname',
    },
    include_paths = "root['name']"
) # expected to be {} and is {}
```

**OS, DeepDiff version and Python version (please complete the following information):**
 - OS: OpenSuSe Leap 15.6 as well as Windows 11
 - Python Version 3.10 (on linux), 3.11 (on windows)
 - DeepDiff Version 8.0.1


Discussion:

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
index 79d9edb..cd3db13 100644
--- a/AUTHORS.md
+++ b/AUTHORS.md
@@ -63,5 +63,3 @@ 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 12da1c2..95cd2c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,6 @@
 
 - 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 7c2e2b4..32fee9c 100644
--- a/deepdiff/deephash.py
+++ b/deepdiff/deephash.py
@@ -24,11 +24,6 @@ try:
     import polars
 except ImportError:
     polars = False
-try:
-    import numpy as np
-    booleanTypes = (bool, np.bool_)
-except ImportError:
-    booleanTypes = bool
 
 logger = logging.getLogger(__name__)
 
@@ -497,7 +492,7 @@ class DeepHash(Base):
         """The main hash method"""
         counts = 1
 
-        if isinstance(obj, booleanTypes):
+        if isinstance(obj, bool):
             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 6437fa3..61284af 100755
--- a/deepdiff/diff.py
+++ b/deepdiff/diff.py
@@ -421,7 +421,7 @@ class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, Base):
                 else:
                     all_slots.extend(slots)
 
-        return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))}
+        return {i: getattr(object, unmangle(i)) 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)
@@ -510,6 +510,32 @@ class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, Base):
 
         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 @@ class DeepDiff(ResultDict, SerializationMixin, DistanceMixin, Base):
             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 e350b3c..5b4075e 100644
--- a/deepdiff/serialization.py
+++ b/deepdiff/serialization.py
@@ -296,7 +296,7 @@ class SerializationMixin:
 
         return deepcopy(dict(result))
 
-    def pretty(self, prefix=None):
+    def pretty(self):
         """
         The pretty human readable string output for the diff object
         regardless of what view was used to generate the diff.
@@ -310,16 +310,12 @@ class SerializationMixin:
             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)]
 
-        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)
+        return '\n'.join(result)
 
 
 class _RestrictedUnpickler(pickle.Unpickler):
diff --git a/docs/view.rst b/docs/view.rst
index 6343590..f50fc9f 100644
--- a/docs/view.rst
+++ b/docs/view.rst
@@ -299,29 +299,6 @@ 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.txt b/requirements.txt
index 53ac539..28bbd74 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,1 @@
-orderly-set>=5.2.3,<6
\ No newline at end of file
+orderly-set==5.2.2
diff --git a/setup.py b/setup.py
index e18d83f..7db28b6 100755
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,9 @@ setup(name='deepdiff',
       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,


Test patch:
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
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
diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py
index 3e5fcc8..ec6f66b 100755
--- a/tests/test_diff_text.py
+++ b/tests/test_diff_text.py
@@ -1713,7 +1713,7 @@ class TestDeepDiffText:
         t2 = Bad()
 
         ddiff = DeepDiff(t1, t2)
-        result = {}
+        result = {'unprocessed': ['root: Bad Object and Bad Object']}
         assert result == ddiff
 
     def test_dict_none_item_removed(self):
diff --git a/tests/test_hash.py b/tests/test_hash.py
index 22a86e2..5263757 100755
--- a/tests/test_hash.py
+++ b/tests/test_hash.py
@@ -187,12 +187,6 @@ class TestDeepHash:
         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 d578e53..facda24 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -330,49 +330,6 @@ class TestDeepDiffPretty:
         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),