Skip to content

Commit

Permalink
Now we only use dict for slots with metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
sobolevn committed Sep 27, 2024
1 parent 9b300ef commit 4e64149
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 36 deletions.
42 changes: 29 additions & 13 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,31 @@ def _update_func_cell_for__class__(f, oldcls, newcls):
return False


def _create_slots(defined_fields, inherited_slots, field_names, weakref_slot):
# The slots for our class. Remove slots from our base classes. Add
# '__weakref__' if weakref_slot was given, unless it is already present.
seen_docs = False
slots = {}
for slot in itertools.filterfalse(
inherited_slots.__contains__,
itertools.chain(
# gh-93521: '__weakref__' also needs to be filtered out if
# already present in inherited_slots
field_names, ('__weakref__',) if weakref_slot else ()
)
):
doc = getattr(defined_fields.get(slot), 'doc', None)
if doc is not None:
seen_docs = True
slots.update({slot: doc})

# We only return dict if there's at least one doc member,
# otherwise we return tuple, which is the old default format.
if seen_docs:
return slots
return tuple(slots)


def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# Need to create a new class, since we can't set __slots__ after a
# class has been created, and the @dataclass decorator is called
Expand All @@ -1258,19 +1283,10 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
inherited_slots = set(
itertools.chain.from_iterable(map(_get_slots, cls.__mro__[1:-1]))
)
# The slots for our class. Remove slots from our base classes. Add
# '__weakref__' if weakref_slot was given, unless it is already present.
cls_dict["__slots__"] = {
slot: getattr(defined_fields.get(slot), 'doc', None)
for slot in itertools.filterfalse(
inherited_slots.__contains__,
itertools.chain(
# gh-93521: '__weakref__' also needs to be filtered out if
# already present in inherited_slots
field_names, ('__weakref__',) if weakref_slot else ()
)
)
}

cls_dict["__slots__"] = _create_slots(
defined_fields, inherited_slots, field_names, weakref_slot,
)

for field_name in field_names:
# Remove our attributes, if present. They'll still be
Expand Down
37 changes: 18 additions & 19 deletions Lib/test/test_dataclasses/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3262,7 +3262,7 @@ class Base(Root4):
j: str
h: str

self.assertEqual(Base.__slots__, {'y': None})
self.assertEqual(Base.__slots__, ('y',))

@dataclass(slots=True)
class Derived(Base):
Expand All @@ -3272,7 +3272,7 @@ class Derived(Base):
k: str
h: str

self.assertEqual(Derived.__slots__, {'z': None})
self.assertEqual(Derived.__slots__, ('z',))

@dataclass
class AnotherDerived(Base):
Expand Down Expand Up @@ -3338,8 +3338,7 @@ class FrozenWithoutSlotsClass:
def test_frozen_pickle(self):
# bpo-43999

self.assertEqual(self.FrozenSlotsClass.__slots__,
{"foo": None, "bar": None})
self.assertEqual(self.FrozenSlotsClass.__slots__, ("foo", "bar"))
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
obj = self.FrozenSlotsClass("a", 1)
Expand Down Expand Up @@ -3576,7 +3575,7 @@ class A:
class B(A):
pass

self.assertEqual(B.__slots__, {})
self.assertEqual(B.__slots__, ())
B()

def test_dataclass_derived_generic(self):
Expand All @@ -3585,14 +3584,14 @@ def test_dataclass_derived_generic(self):
@dataclass(slots=True, weakref_slot=True)
class A(typing.Generic[T]):
pass
self.assertEqual(A.__slots__, {'__weakref__': None})
self.assertEqual(A.__slots__, ('__weakref__',))
self.assertTrue(A.__weakref__)
A()

@dataclass(slots=True, weakref_slot=True)
class B[T2]:
pass
self.assertEqual(B.__slots__, {'__weakref__': None})
self.assertEqual(B.__slots__, ('__weakref__',))
self.assertTrue(B.__weakref__)
B()

Expand All @@ -3604,20 +3603,20 @@ class RawBase: ...
@dataclass(slots=True, weakref_slot=True)
class C1(typing.Generic[T], RawBase):
pass
self.assertEqual(C1.__slots__, {})
self.assertEqual(C1.__slots__, ())
self.assertTrue(C1.__weakref__)
C1()
@dataclass(slots=True, weakref_slot=True)
class C2(RawBase, typing.Generic[T]):
pass
self.assertEqual(C2.__slots__, {})
self.assertEqual(C2.__slots__, ())
self.assertTrue(C2.__weakref__)
C2()

@dataclass(slots=True, weakref_slot=True)
class D[T2](RawBase):
pass
self.assertEqual(D.__slots__, {})
self.assertEqual(D.__slots__, ())
self.assertTrue(D.__weakref__)
D()

Expand All @@ -3630,20 +3629,20 @@ class WithSlots:
@dataclass(slots=True, weakref_slot=True)
class E1(WithSlots, Generic[T]):
pass
self.assertEqual(E1.__slots__, {'__weakref__': None})
self.assertEqual(E1.__slots__, ('__weakref__',))
self.assertTrue(E1.__weakref__)
E1()
@dataclass(slots=True, weakref_slot=True)
class E2(Generic[T], WithSlots):
pass
self.assertEqual(E2.__slots__, {'__weakref__': None})
self.assertEqual(E2.__slots__, ('__weakref__',))
self.assertTrue(E2.__weakref__)
E2()

@dataclass(slots=True, weakref_slot=True)
class F[T2](WithSlots):
pass
self.assertEqual(F.__slots__, {'__weakref__': None})
self.assertEqual(F.__slots__, ('__weakref__',))
self.assertTrue(F.__weakref__)
F()

Expand All @@ -3656,20 +3655,20 @@ class WithWeakrefSlot:
@dataclass(slots=True, weakref_slot=True)
class G1(WithWeakrefSlot, Generic[T]):
pass
self.assertEqual(G1.__slots__, {})
self.assertEqual(G1.__slots__, ())
self.assertTrue(G1.__weakref__)
G1()
@dataclass(slots=True, weakref_slot=True)
class G2(Generic[T], WithWeakrefSlot):
pass
self.assertEqual(G2.__slots__, {})
self.assertEqual(G2.__slots__, ())
self.assertTrue(G2.__weakref__)
G2()

@dataclass(slots=True, weakref_slot=True)
class H[T2](WithWeakrefSlot):
pass
self.assertEqual(H.__slots__, {})
self.assertEqual(H.__slots__, ())
self.assertTrue(H.__weakref__)
H()

Expand All @@ -3680,7 +3679,7 @@ class WithDictSlot:
@dataclass(slots=True)
class A(WithDictSlot): ...

self.assertEqual(A.__slots__, {})
self.assertEqual(A.__slots__, ())
self.assertEqual(A().__dict__, {})
A()

Expand All @@ -3695,13 +3694,13 @@ def test_dataclass_slot_dict_ctype(self):
class HasDictOffset(_testcapi.HeapCTypeWithDict):
__dict__: dict = {}
self.assertNotEqual(_testcapi.HeapCTypeWithDict.__dictoffset__, 0)
self.assertEqual(HasDictOffset.__slots__, {})
self.assertEqual(HasDictOffset.__slots__, ())

@dataclass(slots=True)
class DoesNotHaveDictOffset(_testcapi.HeapCTypeWithWeakref):
__dict__: dict = {}
self.assertEqual(_testcapi.HeapCTypeWithWeakref.__dictoffset__, 0)
self.assertEqual(DoesNotHaveDictOffset.__slots__, {'__dict__': None})
self.assertEqual(DoesNotHaveDictOffset.__slots__, ('__dict__',))

@support.cpython_only
def test_slots_with_wrong_init_subclass(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Add ``doc`` parameter to :func:`dataclasses.field`, so it can be stored and
shown as a documentation / metadata.
It is only visible when ``@dataclass(slots=True)`` is used.
Add *doc* parameter to :func:`dataclasses.field`, so it can be stored and
shown as a documentation / metadata. If ``@dataclass(slots=True)`` is used,
then the supplied string is availabl in the :object:`__slots__` dict. Otherwise,
the supplied string is only available in the corresponding
:class:`dataclasses.Field` object.

In order to support this feature we are changing the ``__slots__`` format
in dataclasses from :class:`tuple` to :class:`dict`.
in dataclasses from :class:`tuple` to :class:`dict`
when documentation / metadata is present.

0 comments on commit 4e64149

Please sign in to comment.