Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
bpo-45897: Fix frozen-slotted dataclass bug
  • Loading branch information
AlexWaygood committed Dec 2, 2021
commit 9c36be6aedb0b7be1e353020bcb3979c90791399
22 changes: 14 additions & 8 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -606,14 +606,18 @@ def _frozen_get_del_attr(cls, fields, globals):
fields_str = '()'
return (_create_fn('__setattr__',
('self', 'name', 'value'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if "__slots__" in cls.__dict__ and name not in cls.__slots__:',
Comment thread
JelleZijlstra marked this conversation as resolved.
' pass',
f'elif type(self) is cls or name in {fields_str}:',
' raise FrozenInstanceError(f"cannot assign to field {name!r}")',
f'super(cls, self).__setattr__(name, value)'),
locals=locals,
globals=globals),
_create_fn('__delattr__',
('self', 'name'),
(f'if type(self) is cls or name in {fields_str}:',
(f'if "__slots__" in cls.__dict__ and name not in cls.__slots__:',
' pass',
f'elif type(self) is cls or name in {fields_str}:',
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
f'super(cls, self).__delattr__(name)'),
locals=locals,
Expand Down Expand Up @@ -1072,12 +1076,6 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
f'in class {cls.__name__}. Consider using '
'functools.total_ordering')

if frozen:
for fn in _frozen_get_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__}')

# Decide if/how we're going to create a hash function.
hash_action = _hash_action[bool(unsafe_hash),
bool(eq),
Expand All @@ -1101,6 +1099,14 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen,
if slots:
cls = _add_slots(cls, frozen)

# Frozen __setattr__/__delattr__ must be added after __slots__
# bpo-45897
if frozen:
for fn in _frozen_get_del_attr(cls, field_list, globals):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function ought to be named _frozen_set_del_attr. That's outside the scope of this PR though.

if _set_new_attribute(cls, fn.__name__, fn):
raise TypeError(f'Cannot overwrite attribute {fn.__name__} '
f'in class {cls.__name__}')

abc.update_abstractmethods(cls)

return cls
Expand Down
39 changes: 39 additions & 0 deletions Lib/test/test_dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -2902,6 +2902,45 @@ class A:
self.assertEqual(obj.a, 'a')
self.assertEqual(obj.b, 'b')

def test_frozen_slots_raises_correct_error(self):
# bpo-45897
Comment thread
AlexWaygood marked this conversation as resolved.
Outdated
@dataclass(slots=True, frozen=True)
class Point:
x: int
y: int

p = Point(1, 2)
self.assertEqual(p.x, 1)
self.assertEqual(p.y, 2)
with self.assertRaises(FrozenInstanceError):
p.x = 2
try:
p.z = 5
except FrozenInstanceError as exc:
raise TypeError(
'FrozenInstanceError unexpectedly raised instead of AttributeError'
) from exc
except AttributeError:
pass
else:
self.fail('AttributeError expected, but no error was raised')

@dataclass(frozen=True)
class DataclassSubclassOfPointWithNoSlots(Point): pass

d = DataclassSubclassOfPointWithNoSlots(1, 2)
with self.assertRaises(FrozenInstanceError):
d.x = 2
with self.assertRaises(FrozenInstanceError):
d.z = 5

class NonDataclassSubclassOfPointWithNoSlots(Point): pass

n = NonDataclassSubclassOfPointWithNoSlots(1, 2)
with self.assertRaises(FrozenInstanceError):
n.x = 2
n.z = 5

class TestDescriptors(unittest.TestCase):
def test_set_name(self):
# See bpo-33141.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix bug in dataclasses where a frozen dataclass with slots would raise
``TypeError`` rather than ``AttributeError`` if a user attempted to
set/delete an attribute that was not specified in ``__slots__``. Patch by
Alex Waygood.