From 8c3377b00d97eb7bd2d06b0995f82132e037777e Mon Sep 17 00:00:00 2001 From: uttam12331 Date: Sun, 5 Jul 2026 18:44:13 +0530 Subject: [PATCH] Fix mangled __slots__ attribute lost for subclass instances `_dict_from_slots` unmangled each slot name using the concrete instance type (`type(object).__name__`). A mangled slot such as `__id` is stored under the name of the class that *declared* it (e.g. `_Parent__id`), so for a subclass instance the lookup used the wrong name (`_Child__id`), `hasattr` returned False, and the parent-defined slot was silently dropped. Pair each slot with the class in the MRO that declared it and unmangle using that class's name. Closes #506 --- CHANGELOG.md | 1 + deepdiff/diff.py | 17 ++++++++++++----- tests/test_diff_text.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46217041..440b2b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # DeepDiff Change log - v9-1-0 + - Fixed `_dict_from_slots` dropping a parent-declared mangled `__slots__` attribute for a subclass instance, because the name was unmangled using the concrete instance type instead of the declaring class (#506) - Added multiprocessing support for DeepDiff: parallel distance computation and parallel subtree diffing with aggregated worker stats, deterministic ordering, and automatic fallback to serial when unsafe (e.g. `custom_operators`, `*_obj_callback`, `ignore_order_func`) - Added wildcard/glob pattern support for `exclude_paths` and `include_paths` thanks to [akshat62](https://github.com/akshat62) - Reimplemented internal cache for improved performance diff --git a/deepdiff/diff.py b/deepdiff/diff.py index e65fd4a0..a49ac322 100755 --- a/deepdiff/diff.py +++ b/deepdiff/diff.py @@ -503,10 +503,13 @@ def _skip_report_for_include_glob(self, level): @staticmethod def _dict_from_slots(object: Any) -> Dict[str, Any]: - def unmangle(attribute: str) -> str: + def unmangle(attribute: str, klass: type) -> str: + # A mangled slot (e.g. ``__id``) is stored under the name of the + # class that declared it, not the concrete instance type, so a + # subclass instance would otherwise lose a parent-defined slot (#506). if attribute.startswith('__') and attribute != '__weakref__': return '_{type}{attribute}'.format( - type=type(object).__name__, + type=klass.__name__, attribute=attribute ) return attribute @@ -522,11 +525,15 @@ def unmangle(attribute: str) -> str: slots = getattr(type_in_mro, '__slots__', None) if slots: if isinstance(slots, strings): - all_slots.append(slots) + all_slots.append((type_in_mro, slots)) else: - all_slots.extend(slots) + all_slots.extend((type_in_mro, i) for i in slots) - return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))} + return { + i: getattr(object, key) + for (klass, i) in all_slots + if hasattr(object, key := unmangle(i, klass)) + } def _diff_enum(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), local_tree: Optional[Any]=None) -> None: t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS) diff --git a/tests/test_diff_text.py b/tests/test_diff_text.py index fb0087be..6853554e 100755 --- a/tests/test_diff_text.py +++ b/tests/test_diff_text.py @@ -823,6 +823,22 @@ class ClassB(ClassA): result = {} assert result == ddiff + def test_dict_from_slots_of_subclass_with_mangled_attribute(self): + # A mangled slot (e.g. ``__id``) declared on a parent class is stored + # under the parent's mangled name, so it must be unmangled using the + # declaring class, not the concrete instance type. Otherwise a subclass + # instance silently loses the parent-defined slot. See #506. + class ClassA: + __slots__ = ('__id',) + + def __init__(self, id): + self.__id = id + + class ClassB(ClassA): + pass + + assert DeepDiff._dict_from_slots(ClassA(5)) == {'__id': 5} + assert DeepDiff._dict_from_slots(ClassB(5)) == {'__id': 5} def test_custom_class_changes_none_when_ignore_type(self): ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)])