Skip to content

Commit 4338b06

Browse files
committed
The PR #577 used hasattr(o, i) to skip uninitialized slots, but hasattr() swallows all
AttributeErrors — including those from __getattr__. This made objects with a broken __getattr__ (like the Bad test class) appear to have zero accessible slots rather than failing the strategy. The fix uses object.__getattribute__() to check each slot directly, which bypasses __getattr__ entirely. For classes that do define __getattr__, if a slot isn't initialized via the descriptor, we fall back to the normal getattr() — letting __getattr__ either provide a value or raise (propagating the failure). This correctly handles: - Uninitialized slots (no __getattr__): skipped gracefully, empty dict is valid - Partially initialized slots: only initialized ones included - Broken __getattr__: error propagates, object becomes unprocessed
1 parent f0b39cb commit 4338b06

File tree

1 file changed

+22
-1
lines changed

1 file changed

+22
-1
lines changed

deepdiff/deephash.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,27 @@ def __getitem__(self, obj: Any, extract_index: Optional[int] = 0) -> Any:
280280
use_enum_value=self.use_enum_value,
281281
ignore_numeric_type_changes=self.ignore_numeric_type_changes)
282282

283+
@staticmethod
284+
def _get_slots_dict(obj: Any) -> Dict[str, Any]:
285+
"""Get a dict of initialized slot attributes.
286+
287+
Uses object.__getattribute__ to check each slot directly, bypassing
288+
__getattr__. For uninitialized slots on classes that define __getattr__,
289+
falls back to getattr — letting it raise if the object is truly broken.
290+
"""
291+
result = {}
292+
has_getattr = hasattr(type(obj), '__getattr__')
293+
for slot in obj.__slots__:
294+
try:
295+
result[slot] = object.__getattribute__(obj, slot)
296+
except AttributeError:
297+
if has_getattr:
298+
# The slot isn't initialized, but the class defines __getattr__.
299+
# Try the normal getattr to let __getattr__ provide a value or
300+
# raise — if it raises, we propagate to fail the strategy.
301+
result[slot] = getattr(obj, slot)
302+
return result
303+
283304
@staticmethod
284305
def _getitem(hashes: Dict[Any, Any], obj: Any, extract_index: Optional[int] = 0,
285306
use_enum_value: bool = False, ignore_numeric_type_changes: bool = False) -> Any:
@@ -415,7 +436,7 @@ def _prep_obj(self, obj: Any, parent: str, parents_ids: frozenset = EMPTY_FROZEN
415436
obj_to_dict_strategies.append(lambda o: o.__dict__)
416437

417438
if hasattr(obj, "__slots__"):
418-
obj_to_dict_strategies.append(lambda o: {i: getattr(o, i) for i in o.__slots__ if hasattr(o, i)})
439+
obj_to_dict_strategies.append(lambda o: DeepHash._get_slots_dict(o))
419440
else:
420441
import inspect
421442
obj_to_dict_strategies.append(lambda o: dict(inspect.getmembers(o, lambda m: not inspect.isroutine(m))))

0 commit comments

Comments
 (0)