diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index e766a7b554afe1..ae8b159cc711c2 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -635,7 +635,7 @@ def _repr_fn(fields, globals): return _recursive_repr(fn) -def _frozen_get_del_attr(cls, fields, globals): +def _frozen_set_del_attr(cls, fields, globals): locals = {'cls': cls, 'FrozenInstanceError': FrozenInstanceError} condition = 'type(self) is cls' @@ -1055,6 +1055,12 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, (std_init_fields, kw_only_init_fields) = _fields_in_init_order(all_init_fields) + # It's an error to specify weakref_slot if slots is False. + if weakref_slot and not slots: + raise TypeError('weakref_slot is True but slots is False') + if slots: + cls = _add_slots(cls, frozen, weakref_slot) + if init: # Does this class have a post-init function? has_post_init = hasattr(cls, _POST_INIT_NAME) @@ -1115,7 +1121,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, 'functools.total_ordering') if frozen: - for fn in _frozen_get_del_attr(cls, field_list, globals): + for fn in _frozen_set_del_attr(cls, field_list, globals): if _set_new_attribute(cls, fn.__name__, fn): raise TypeError(f'Cannot overwrite attribute {fn.__name__} ' f'in class {cls.__name__}') @@ -1145,12 +1151,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen, _set_new_attribute(cls, '__match_args__', tuple(f.name for f in std_init_fields)) - # It's an error to specify weakref_slot if slots is False. - if weakref_slot and not slots: - raise TypeError('weakref_slot is True but slots is False') - if slots: - cls = _add_slots(cls, frozen, weakref_slot) - abc.update_abstractmethods(cls) return cls diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 6669f1c57e2e78..2176324e66f433 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2820,6 +2820,23 @@ class C: self.assertEqual(c.i, 10) with self.assertRaises(FrozenInstanceError): c.i = 5 + with self.assertRaises(FrozenInstanceError): + c.j = 5 + + self.assertEqual(c.i, 10) + + def test_frozen_with_slots(self): + @dataclass(frozen=True, slots=True) + class C: + i: int + + c = C(10) + self.assertEqual(c.i, 10) + with self.assertRaises(FrozenInstanceError): + c.i = 5 + with self.assertRaises(FrozenInstanceError): + c.j = 5 + self.assertEqual(c.i, 10) def test_frozen_empty(self): @@ -2970,6 +2987,43 @@ class S(D): del s.cached self.assertNotIsInstance(cm.exception, FrozenInstanceError) + def test_non_frozen_normal_derived_with_slots(self): + # See bpo-32953. + + @dataclass(frozen=True, slots=True) + class D: + x: int + y: int = 10 + + class S(D): + pass + + s = S(3) + self.assertEqual(s.x, 3) + self.assertEqual(s.y, 10) + s.cached = True + + # But can't change the frozen attributes. + with self.assertRaises(FrozenInstanceError): + s.x = 5 + with self.assertRaises(FrozenInstanceError): + s.y = 5 + self.assertEqual(s.x, 3) + self.assertEqual(s.y, 10) + self.assertEqual(s.cached, True) + + with self.assertRaises(FrozenInstanceError): + del s.x + self.assertEqual(s.x, 3) + with self.assertRaises(FrozenInstanceError): + del s.y + self.assertEqual(s.y, 10) + del s.cached + self.assertFalse(hasattr(s, 'cached')) + with self.assertRaises(AttributeError) as cm: + del s.cached + self.assertNotIsInstance(cm.exception, FrozenInstanceError) + def test_non_frozen_normal_derived_from_empty_frozen(self): @dataclass(frozen=True) class D: diff --git a/Misc/NEWS.d/next/Library/2023-06-20-10-14-29.gh-issue-105936.7VfFJW.rst b/Misc/NEWS.d/next/Library/2023-06-20-10-14-29.gh-issue-105936.7VfFJW.rst new file mode 100644 index 00000000000000..dddd2c8bb53c27 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-20-10-14-29.gh-issue-105936.7VfFJW.rst @@ -0,0 +1 @@ +Fixed frozen dataclasses with slots' ``__setattr__`` and ``__delattr_``.