Skip to content

Commit 269a971

Browse files
committed
Merge branch 'dev' of github.com:seperman/deepdiff into dev
2 parents 2d61bb1 + 32d60a9 commit 269a971

10 files changed

+120
-10
lines changed

AUTHORS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,5 @@ Authors in order of the timeline of their contributions:
6363
- [sf-tcalhoun](https://github.com/sf-tcalhoun) for fixing "Instantiating a Delta with a flat_dict_list unexpectedly mutates the flat_dict_list"
6464
- [dtorres-sf](https://github.com/dtorres-sf) for fixing iterable moved items when iterable_compare_func is used.
6565
- [Florian Finkernagel](https://github.com/TyberiusPrime) for pandas and polars support.
66+
- Mathis Chenuet [artemisart](https://github.com/artemisart) for fixing slots classes comparison.
67+
- [Aaron D. Marasco](https://github.com/AaronDMarasco) added `prefix` option to `pretty()`

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
- v8-0-1
55
- Bugfix. Numpy should be optional.
6+
- Added `prefix` option to `pretty()`
67

78
- v8-0-0
89

deepdiff/deephash.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
import polars
2525
except ImportError:
2626
polars = False
27+
try:
28+
import numpy as np
29+
booleanTypes = (bool, np.bool_)
30+
except ImportError:
31+
booleanTypes = bool
2732

2833
logger = logging.getLogger(__name__)
2934

@@ -492,7 +497,7 @@ def _hash(self, obj, parent, parents_ids=EMPTY_FROZENSET):
492497
"""The main hash method"""
493498
counts = 1
494499

495-
if isinstance(obj, bool):
500+
if isinstance(obj, booleanTypes):
496501
obj = self._prep_bool(obj)
497502
result = None
498503
elif self.use_enum_value and isinstance(obj, Enum):

deepdiff/diff.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ def unmangle(attribute):
421421
else:
422422
all_slots.extend(slots)
423423

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

426426
def _diff_enum(self, level, parents_ids=frozenset(), local_tree=None):
427427
t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS)
@@ -510,6 +510,32 @@ def _skip_this(self, level):
510510

511511
return skip
512512

513+
def _skip_this_key(self, level, key):
514+
# if include_paths is not set, than treet every path as included
515+
if self.include_paths is None:
516+
return False
517+
if "{}['{}']".format(level.path(), key) in self.include_paths:
518+
return False
519+
if level.path() in self.include_paths:
520+
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']"]
521+
return False
522+
for prefix in self.include_paths:
523+
if "{}['{}']".format(level.path(), key) in prefix:
524+
# matches as long the prefix is longer than this object key
525+
# eg.: level+key root['foo']['bar'] matches prefix root['foo']['bar'] from include paths
526+
# level+key root['foo'] matches prefix root['foo']['bar'] from include_paths
527+
# level+key root['foo']['bar'] DOES NOT match root['foo'] from include_paths This needs to be handled afterwards
528+
return False
529+
# check if a higher level is included as a whole (=without any sublevels specified)
530+
# matches e.g. level+key root['foo']['bar']['veg'] include_paths ["root['foo']"]
531+
# but does not match, if it is level+key root['foo']['bar']['veg'] include_paths ["root['foo']['bar']['fruits']"]
532+
up = level.up
533+
while up is not None:
534+
if up.path() in self.include_paths:
535+
return False
536+
up = up.up
537+
return True
538+
513539
def _get_clean_to_keys_mapping(self, keys, level):
514540
"""
515541
Get a dictionary of cleaned value of keys to the keys themselves.
@@ -570,11 +596,11 @@ def _diff_dict(
570596
rel_class = DictRelationship
571597

572598
if self.ignore_private_variables:
573-
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__'))])
574-
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__'))])
599+
t1_keys = SetOrdered([key for key in t1 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
600+
t2_keys = SetOrdered([key for key in t2 if not(isinstance(key, str) and key.startswith('__')) and not self._skip_this_key(level, key)])
575601
else:
576-
t1_keys = SetOrdered(t1.keys())
577-
t2_keys = SetOrdered(t2.keys())
602+
t1_keys = SetOrdered([key for key in t1 if not self._skip_this_key(level, key)])
603+
t2_keys = SetOrdered([key for key in t2 if not self._skip_this_key(level, key)])
578604
if self.ignore_string_type_changes or self.ignore_numeric_type_changes or self.ignore_string_case:
579605
t1_clean_to_keys = self._get_clean_to_keys_mapping(keys=t1_keys, level=level)
580606
t2_clean_to_keys = self._get_clean_to_keys_mapping(keys=t2_keys, level=level)

deepdiff/serialization.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def _to_delta_dict(self, directed=True, report_repetition_required=True, always_
296296

297297
return deepcopy(dict(result))
298298

299-
def pretty(self):
299+
def pretty(self, prefix=None):
300300
"""
301301
The pretty human readable string output for the diff object
302302
regardless of what view was used to generate the diff.
@@ -310,12 +310,16 @@ def pretty(self):
310310
Item root[1] removed from set.
311311
"""
312312
result = []
313+
if prefix is None:
314+
prefix = ''
313315
keys = sorted(self.tree.keys()) # sorting keys to guarantee constant order across python versions.
314316
for key in keys:
315317
for item_key in self.tree[key]:
316318
result += [pretty_print_diff(item_key)]
317319

318-
return '\n'.join(result)
320+
if callable(prefix):
321+
return "\n".join(f"{prefix(diff=self)}{r}" for r in result)
322+
return "\n".join(f"{prefix}{r}" for r in result)
319323

320324

321325
class _RestrictedUnpickler(pickle.Unpickler):

docs/view.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,29 @@ Use the pretty method for human readable output. This is regardless of what view
299299
Item root[4] removed from set.
300300
Item root[1] removed from set.
301301

302+
The pretty method has an optional parameter ``prefix`` that allows a prefix string before every output line (*e.g.* for logging):
303+
>>> from deepdiff import DeepDiff
304+
>>> t1={1,2,4}
305+
>>> t2={2,3}
306+
>>> print(DeepDiff(t1, t2).pretty(prefix='Diff: '))
307+
Diff: Item root[3] added to set.
308+
Diff: Item root[4] removed from set.
309+
Diff: Item root[1] removed from set.
310+
311+
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.
312+
The ``diff`` given will be the ``DeepDiff`` that ``pretty`` was called on; this allows interesting capabilities such as:
313+
>>> from deepdiff import DeepDiff
314+
>>> t1={1,2,4}
315+
>>> t2={2,3}
316+
>>> def callback(**kwargs):
317+
... """Helper function using a hidden variable on the diff that tracks which count prints next"""
318+
... kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0)
319+
... return f"Diff #{kwargs['diff']._diff_count}: "
320+
...
321+
>>> print(DeepDiff(t1, t2).pretty(prefix=callback))
322+
Diff #1: Item root[3] added to set.
323+
Diff #2: Item root[4] removed from set.
324+
Diff #3: Item root[1] removed from set.
302325

303326

304327
Text view vs. Tree view vs. vs. pretty() method

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
orderly-set==5.2.3
1+
orderly-set>=5.2.3,<6

tests/test_diff_text.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1766,7 +1766,7 @@ def __str__(self):
17661766
t2 = Bad()
17671767

17681768
ddiff = DeepDiff(t1, t2)
1769-
result = {'unprocessed': ['root: Bad Object and Bad Object']}
1769+
result = {}
17701770
assert result == ddiff
17711771

17721772
def test_dict_none_item_removed(self):

tests/test_hash.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,12 @@ def test_re(self):
187187
a_hash = DeepHash(a)[a]
188188
assert not( a_hash is unprocessed)
189189

190+
# https://github.com/seperman/deepdiff/issues/494
191+
def test_numpy_bool(self):
192+
a = {'b': np.array([True], dtype='bool')}
193+
a_hash = DeepHash(a)[a]
194+
assert not( a_hash is unprocessed)
195+
190196
class TestDeepHashPrep:
191197
"""DeepHashPrep Tests covering object serialization."""
192198

tests/test_serialization.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,49 @@ def test_pretty_form_method(self, expected, verbose_level):
330330
result = ddiff.pretty()
331331
assert result == expected
332332

333+
@pytest.mark.parametrize("expected, verbose_level",
334+
(
335+
('\t\tItem root[5] added to dictionary.'
336+
'\n\t\tItem root[3] removed from dictionary.'
337+
'\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".'
338+
'\n\t\tValue of root[4] changed from 4 to 5.', 0),
339+
('\t\tItem root[5] (5) added to dictionary.'
340+
'\n\t\tItem root[3] (3) removed from dictionary.'
341+
'\n\t\tType of root[2] changed from int to str and value changed from 2 to "b".'
342+
'\n\t\tValue of root[4] changed from 4 to 5.', 2),
343+
), ids=("verbose=0", "verbose=2")
344+
)
345+
def test_pretty_form_method_prefixed_simple(self, expected, verbose_level):
346+
t1 = {2: 2, 3: 3, 4: 4}
347+
t2 = {2: 'b', 4: 5, 5: 5}
348+
ddiff = DeepDiff(t1, t2, verbose_level=verbose_level)
349+
result = ddiff.pretty(prefix="\t\t")
350+
assert result == expected
351+
352+
@pytest.mark.parametrize("expected, verbose_level",
353+
(
354+
('Diff #1: Item root[5] added to dictionary.'
355+
'\nDiff #2: Item root[3] removed from dictionary.'
356+
'\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".'
357+
'\nDiff #4: Value of root[4] changed from 4 to 5.', 0),
358+
('Diff #1: Item root[5] (5) added to dictionary.'
359+
'\nDiff #2: Item root[3] (3) removed from dictionary.'
360+
'\nDiff #3: Type of root[2] changed from int to str and value changed from 2 to "b".'
361+
'\nDiff #4: Value of root[4] changed from 4 to 5.', 2),
362+
), ids=("verbose=0", "verbose=2")
363+
)
364+
def test_pretty_form_method_prefixed_callback(self, expected, verbose_level):
365+
def prefix_callback(**kwargs):
366+
"""Helper function using a hidden variable on the diff that tracks which count prints next"""
367+
kwargs['diff']._diff_count = 1 + getattr(kwargs['diff'], '_diff_count', 0)
368+
return f"Diff #{kwargs['diff']._diff_count}: "
369+
370+
t1 = {2: 2, 3: 3, 4: 4}
371+
t2 = {2: 'b', 4: 5, 5: 5}
372+
ddiff = DeepDiff(t1, t2, verbose_level=verbose_level)
373+
result = ddiff.pretty(prefix=prefix_callback)
374+
assert result == expected
375+
333376
@pytest.mark.parametrize('test_num, value, func_to_convert_back', [
334377
(1, {'10': None}, None),
335378
(2, {"type_changes": {"root": {"old_type": None, "new_type": list, "new_value": ["你好", 2, 3, 5]}}}, None),

0 commit comments

Comments
 (0)