Skip to content

Commit e953956

Browse files
[3.12] gh-118033: Fix __weakref__ not set for generic dataclasses (GH-118099) (#118822)
gh-118033: Fix `__weakref__` not set for generic dataclasses (GH-118099) (cherry picked from commit fa9b9cb) Co-authored-by: Nikita Sobolev <[email protected]>
1 parent 530c3bb commit e953956

File tree

3 files changed

+118
-3
lines changed

3 files changed

+118
-3
lines changed

Lib/dataclasses.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1168,10 +1168,17 @@ def _dataclass_setstate(self, state):
11681168

11691169
def _get_slots(cls):
11701170
match cls.__dict__.get('__slots__'):
1171-
# A class which does not define __slots__ at all is equivalent
1172-
# to a class defining __slots__ = ('__dict__', '__weakref__')
1171+
# `__dictoffset__` and `__weakrefoffset__` can tell us whether
1172+
# the base type has dict/weakref slots, in a way that works correctly
1173+
# for both Python classes and C extension types. Extension types
1174+
# don't use `__slots__` for slot creation
11731175
case None:
1174-
yield from ('__dict__', '__weakref__')
1176+
slots = []
1177+
if getattr(cls, '__weakrefoffset__', -1) != 0:
1178+
slots.append('__weakref__')
1179+
if getattr(cls, '__dictrefoffset__', -1) != 0:
1180+
slots.append('__dict__')
1181+
yield from slots
11751182
case str(slot):
11761183
yield slot
11771184
# Slots may be any iterable, but we cannot handle an iterator

Lib/test/test_dataclasses/__init__.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3411,8 +3411,114 @@ class A:
34113411
class B(A):
34123412
pass
34133413

3414+
self.assertEqual(B.__slots__, ())
34143415
B()
34153416

3417+
def test_dataclass_derived_generic(self):
3418+
T = typing.TypeVar('T')
3419+
3420+
@dataclass(slots=True, weakref_slot=True)
3421+
class A(typing.Generic[T]):
3422+
pass
3423+
self.assertEqual(A.__slots__, ('__weakref__',))
3424+
self.assertTrue(A.__weakref__)
3425+
A()
3426+
3427+
@dataclass(slots=True, weakref_slot=True)
3428+
class B[T2]:
3429+
pass
3430+
self.assertEqual(B.__slots__, ('__weakref__',))
3431+
self.assertTrue(B.__weakref__)
3432+
B()
3433+
3434+
def test_dataclass_derived_generic_from_base(self):
3435+
T = typing.TypeVar('T')
3436+
3437+
class RawBase: ...
3438+
3439+
@dataclass(slots=True, weakref_slot=True)
3440+
class C1(typing.Generic[T], RawBase):
3441+
pass
3442+
self.assertEqual(C1.__slots__, ())
3443+
self.assertTrue(C1.__weakref__)
3444+
C1()
3445+
@dataclass(slots=True, weakref_slot=True)
3446+
class C2(RawBase, typing.Generic[T]):
3447+
pass
3448+
self.assertEqual(C2.__slots__, ())
3449+
self.assertTrue(C2.__weakref__)
3450+
C2()
3451+
3452+
@dataclass(slots=True, weakref_slot=True)
3453+
class D[T2](RawBase):
3454+
pass
3455+
self.assertEqual(D.__slots__, ())
3456+
self.assertTrue(D.__weakref__)
3457+
D()
3458+
3459+
def test_dataclass_derived_generic_from_slotted_base(self):
3460+
T = typing.TypeVar('T')
3461+
3462+
class WithSlots:
3463+
__slots__ = ('a', 'b')
3464+
3465+
@dataclass(slots=True, weakref_slot=True)
3466+
class E1(WithSlots, Generic[T]):
3467+
pass
3468+
self.assertEqual(E1.__slots__, ('__weakref__',))
3469+
self.assertTrue(E1.__weakref__)
3470+
E1()
3471+
@dataclass(slots=True, weakref_slot=True)
3472+
class E2(Generic[T], WithSlots):
3473+
pass
3474+
self.assertEqual(E2.__slots__, ('__weakref__',))
3475+
self.assertTrue(E2.__weakref__)
3476+
E2()
3477+
3478+
@dataclass(slots=True, weakref_slot=True)
3479+
class F[T2](WithSlots):
3480+
pass
3481+
self.assertEqual(F.__slots__, ('__weakref__',))
3482+
self.assertTrue(F.__weakref__)
3483+
F()
3484+
3485+
def test_dataclass_derived_generic_from_slotted_base(self):
3486+
T = typing.TypeVar('T')
3487+
3488+
class WithWeakrefSlot:
3489+
__slots__ = ('__weakref__',)
3490+
3491+
@dataclass(slots=True, weakref_slot=True)
3492+
class G1(WithWeakrefSlot, Generic[T]):
3493+
pass
3494+
self.assertEqual(G1.__slots__, ())
3495+
self.assertTrue(G1.__weakref__)
3496+
G1()
3497+
@dataclass(slots=True, weakref_slot=True)
3498+
class G2(Generic[T], WithWeakrefSlot):
3499+
pass
3500+
self.assertEqual(G2.__slots__, ())
3501+
self.assertTrue(G2.__weakref__)
3502+
G2()
3503+
3504+
@dataclass(slots=True, weakref_slot=True)
3505+
class H[T2](WithWeakrefSlot):
3506+
pass
3507+
self.assertEqual(H.__slots__, ())
3508+
self.assertTrue(H.__weakref__)
3509+
H()
3510+
3511+
def test_dataclass_slot_dict(self):
3512+
class WithDictSlot:
3513+
__slots__ = ('__dict__',)
3514+
3515+
@dataclass(slots=True)
3516+
class A(WithDictSlot): ...
3517+
3518+
self.assertEqual(A.__slots__, ())
3519+
self.assertEqual(A().__dict__, {})
3520+
A()
3521+
34163522

34173523
class TestDescriptors(unittest.TestCase):
34183524
def test_set_name(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :func:`dataclasses.dataclass` not creating a ``__weakref__`` slot when
2+
subclassing :class:`typing.Generic`.

0 commit comments

Comments
 (0)