Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# DeepDiff Change log

- Unreleased
- Fixed DeepHash omitting inherited and string-declared slot attributes, which could hide changes in unordered comparisons of slotted objects.
- Fixed missing type changes between equal-comparing items inside ordered iterables, e.g. `DeepDiff([2], [2.0])` now reports the `int` → `float` change like `DeepDiff(2, 2.0)` and `DeepDiff({'a': 2}, {'a': 2.0})` already do (issue #605).

- v9-1-0
Expand Down
24 changes: 14 additions & 10 deletions deepdiff/deephash.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,23 +292,27 @@ def __getitem__(self, obj: Any, extract_index: Optional[int] = 0) -> Any:

@staticmethod
def _get_slots_dict(obj: Any) -> Dict[str, Any]:
"""Get a dict of initialized slot attributes.
"""Get a dict of initialized slot attributes across the class hierarchy.

Uses object.__getattribute__ to check each slot directly, bypassing
__getattr__. For uninitialized slots on classes that define __getattr__,
falls back to getattr — letting it raise if the object is truly broken.
"""
result = {}
has_getattr = hasattr(type(obj), '__getattr__')
for slot in obj.__slots__:
try:
result[slot] = object.__getattribute__(obj, slot)
except AttributeError:
if has_getattr:
# The slot isn't initialized, but the class defines __getattr__.
# Try the normal getattr to let __getattr__ provide a value or
# raise — if it raises, we propagate to fail the strategy.
result[slot] = getattr(obj, slot)
for cls in type(obj).__mro__:
slots = cls.__dict__.get('__slots__', ())
if isinstance(slots, str):
slots = (slots,)
for slot in slots:
try:
result[slot] = object.__getattribute__(obj, slot)
except AttributeError:
if has_getattr:
# The slot isn't initialized, but the class defines __getattr__.
# Try the normal getattr to let __getattr__ provide a value or
# raise — if it raises, we propagate to fail the strategy.
result[slot] = getattr(obj, slot)
return result

@staticmethod
Expand Down
95 changes: 95 additions & 0 deletions tests/test_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,101 @@ def __init__(self, x, y):
# Should still not raise AttributeError internally for uninitialized 'z'
assert result[obj] is not unprocessed

@pytest.mark.parametrize('derived_slots', [(), ('y', 'uninitialized'), 'value'])
@pytest.mark.parametrize('nested', [False, True])
def test_inherited_slot_values_affect_hash_and_unordered_diff(self, derived_slots, nested):
class Base:
__slots__ = ('x',)

def __init__(self, x):
self.x = x

class Derived(Base):
__slots__ = derived_slots

class Leaf(Derived):
__slots__ = ('z',)

cls = Leaf if nested else Derived
old = cls(1)
equal = cls(1)
new = cls(3)
for obj in (old, equal, new):
if derived_slots == 'value':
obj.value = 2
elif derived_slots:
obj.y = 2
if nested:
obj.z = 4

hashes = DeepHash([old, equal, new])
assert hashes[old] == hashes[equal]
assert hashes[old] != hashes[new]
assert DeepDiff([old], [equal], ignore_order=True) == {}
assert DeepDiff([old], [new], ignore_order=True) == {
'values_changed': {'root[0].x': {'old_value': 1, 'new_value': 3}}
}

@pytest.mark.parametrize('slots', ['value', {'value': 'Stored value'}])
def test_slot_declaration_values_affect_hash(self, slots):
class Slotted:
__slots__ = slots

def __init__(self, value):
self.value = value

old = Slotted(1)
equal = Slotted(1)
new = Slotted(3)
hashes = DeepHash([old, equal, new])
assert hashes[old] == hashes[equal]
assert hashes[old] != hashes[new]
assert DeepDiff([old], [new], ignore_order=True) == {
'values_changed': {'root[0].value': {'old_value': 1, 'new_value': 3}}
}

@pytest.mark.parametrize('broken', [False, True])
def test_inherited_uninitialized_slot_with_getattr(self, broken):
class Base:
__slots__ = ('x', 'y')

def __init__(self, x):
self.x = x

def __getattr__(self, name):
if name == 'y' and not broken:
return 2
raise AttributeError(name)

class Derived(Base):
__slots__ = ()

old = Derived(1)
new = Derived(3)
hashes = DeepHash([old, new])
if broken:
assert hashes[old] is unprocessed
assert hashes[new] is unprocessed
else:
assert hashes[old] != hashes[new]

def test_inherited_slots_respect_ignored_type_groups(self):
class Base:
__slots__ = ('x',)

def __init__(self, x):
self.x = x

class Derived(Base):
__slots__ = ()

old = Base(1)
new = Derived(1)
options = {'ignore_type_in_groups': [(Base, Derived)]}
hashes = DeepHash([old, new], **options)
assert hashes[old] == hashes[new]
assert DeepDiff([old], [new], ignore_order=True, **options) == {}

def test_slots_deepdiff_comparison(self):
"""Test that DeepDiff also works correctly with __slots__ classes (incl inherited and uninitialized attributes)."""
class Base:
Expand Down