Skip to content

Commit 8c3377b

Browse files
committed
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
1 parent c59636c commit 8c3377b

3 files changed

Lines changed: 29 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# DeepDiff Change log
22

33
- v9-1-0
4+
- 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)
45
- 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`)
56
- Added wildcard/glob pattern support for `exclude_paths` and `include_paths` thanks to [akshat62](https://github.com/akshat62)
67
- Reimplemented internal cache for improved performance

deepdiff/diff.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -503,10 +503,13 @@ def _skip_report_for_include_glob(self, level):
503503

504504
@staticmethod
505505
def _dict_from_slots(object: Any) -> Dict[str, Any]:
506-
def unmangle(attribute: str) -> str:
506+
def unmangle(attribute: str, klass: type) -> str:
507+
# A mangled slot (e.g. ``__id``) is stored under the name of the
508+
# class that declared it, not the concrete instance type, so a
509+
# subclass instance would otherwise lose a parent-defined slot (#506).
507510
if attribute.startswith('__') and attribute != '__weakref__':
508511
return '_{type}{attribute}'.format(
509-
type=type(object).__name__,
512+
type=klass.__name__,
510513
attribute=attribute
511514
)
512515
return attribute
@@ -522,11 +525,15 @@ def unmangle(attribute: str) -> str:
522525
slots = getattr(type_in_mro, '__slots__', None)
523526
if slots:
524527
if isinstance(slots, strings):
525-
all_slots.append(slots)
528+
all_slots.append((type_in_mro, slots))
526529
else:
527-
all_slots.extend(slots)
530+
all_slots.extend((type_in_mro, i) for i in slots)
528531

529-
return {i: getattr(object, key) for i in all_slots if hasattr(object, key := unmangle(i))}
532+
return {
533+
i: getattr(object, key)
534+
for (klass, i) in all_slots
535+
if hasattr(object, key := unmangle(i, klass))
536+
}
530537

531538
def _diff_enum(self, level: Any, parents_ids: FrozenSet[int]=frozenset(), local_tree: Optional[Any]=None) -> None:
532539
t1 = detailed__dict__(level.t1, include_keys=ENUM_INCLUDE_KEYS)

tests/test_diff_text.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,22 @@ class ClassB(ClassA):
823823
result = {}
824824
assert result == ddiff
825825

826+
def test_dict_from_slots_of_subclass_with_mangled_attribute(self):
827+
# A mangled slot (e.g. ``__id``) declared on a parent class is stored
828+
# under the parent's mangled name, so it must be unmangled using the
829+
# declaring class, not the concrete instance type. Otherwise a subclass
830+
# instance silently loses the parent-defined slot. See #506.
831+
class ClassA:
832+
__slots__ = ('__id',)
833+
834+
def __init__(self, id):
835+
self.__id = id
836+
837+
class ClassB(ClassA):
838+
pass
839+
840+
assert DeepDiff._dict_from_slots(ClassA(5)) == {'__id': 5}
841+
assert DeepDiff._dict_from_slots(ClassB(5)) == {'__id': 5}
826842

827843
def test_custom_class_changes_none_when_ignore_type(self):
828844
ddiff1 = DeepDiff({'a': None}, {'a': 1}, ignore_type_subclasses=True, ignore_type_in_groups=[(int, float)])

0 commit comments

Comments
 (0)