From 35cacc4cb8c3e7cec97810a119b41c156102a327 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 26 Apr 2025 15:09:13 +0300 Subject: [PATCH] gh-132946: Do not allow setting data descriptors in `@dataclass(slots=True`) --- Lib/dataclasses.py | 16 ++++++- Lib/test/test_dataclasses/__init__.py | 47 +++++++++++++++++++ ...-04-26-15-01-09.gh-issue-132946.2q6MNT.rst | 2 + 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-04-26-15-01-09.gh-issue-132946.2q6MNT.rst diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 0f7dc9ae6b82f5..4e146085ed1c5b 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -766,7 +766,7 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): return False -def _get_field(cls, a_name, a_type, default_kw_only): +def _get_field(cls, a_name, a_type, default_kw_only, slots): # Return a Field object for this field name and type. ClassVars and # InitVars are also returned, but marked as such (see f._field_type). # default_kw_only is the value of kw_only to use if there isn't a field() @@ -861,6 +861,18 @@ def _get_field(cls, a_name, a_type, default_kw_only): raise ValueError(f'mutable default {type(f.default)} for field ' f'{f.name} is not allowed: use default_factory') + # Validate that you can't set descriptors with `__set__` + # when using `slots=True`. Because `__slots__` will override + # this descriptor and it can hide a bug from users. + if slots: + static_default = inspect.getattr_static(cls, a_name, MISSING) + if ( + not inspect.ismemberdescriptor(static_default) + and inspect.isdatadescriptor(static_default) + ): + raise ValueError(f'data descriptor {type(static_default).__name__!r} ' + f'in {f.name!r} will be overriden when slots=True') + return f def _set_new_attribute(cls, name, value): @@ -1007,7 +1019,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, kw_only = True else: # Otherwise it's a field of some type. - cls_fields.append(_get_field(cls, name, type, kw_only)) + cls_fields.append(_get_field(cls, name, type, kw_only, slots)) for f in cls_fields: fields[f.name] = f diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py index 99fefb57fd0f09..6ca4c9ec6b7056 100644 --- a/Lib/test/test_dataclasses/__init__.py +++ b/Lib/test/test_dataclasses/__init__.py @@ -3383,6 +3383,53 @@ class Root2(Root): class C(Root2): x: int + def test_data_descriptor_with_slots(self): + class DescriptorFieldType: + def __get__(self, instance, owner): + return 1 + + def __set__(self, instance, value): + ... # this method must just be present + + @dataclass + class Regular: + one: DescriptorFieldType = DescriptorFieldType() + + self.assertEqual(Regular.one, 1) + self.assertEqual(Regular().one, 1) + + with self.assertRaisesRegex( + ValueError, + ( + "data descriptor 'DescriptorFieldType' in 'two' " + "will be overriden when slots=True" + ), + ): + @dataclass(slots=True) + class WithSlots: + two: DescriptorFieldType = DescriptorFieldType() + + def test_regular_descriptor_with_slots(self): + class DescriptorFieldType: + def __get__(self, instance, owner): + return 1 + + # no __set__ + + @dataclass + class Regular: + one: DescriptorFieldType = DescriptorFieldType() + + self.assertEqual(Regular.one, 1) + self.assertEqual(Regular().one, 1) + + @dataclass(slots=True) + class WithSlots: + two: DescriptorFieldType = DescriptorFieldType() + + self.assertIsInstance(WithSlots.two, types.MemberDescriptorType) + self.assertEqual(WithSlots().two, 1) + def test_returns_new_class(self): class A: x: int diff --git a/Misc/NEWS.d/next/Library/2025-04-26-15-01-09.gh-issue-132946.2q6MNT.rst b/Misc/NEWS.d/next/Library/2025-04-26-15-01-09.gh-issue-132946.2q6MNT.rst new file mode 100644 index 00000000000000..36eda0e8ff13a4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-26-15-01-09.gh-issue-132946.2q6MNT.rst @@ -0,0 +1,2 @@ +Forbid setting :term:`Data descriptors ` in +:func:`dataclasses.dataclass` with ``slots=True``