Skip to content

Commit 2fa6b9e

Browse files
authored
bpo-32960: For dataclasses, disallow inheriting frozen from non-frozen classes and vice-versa, (GH-5919)
This restriction will be relaxed at a future date.
1 parent 72d9b2b commit 2fa6b9e

File tree

3 files changed

+77
-29
lines changed

3 files changed

+77
-29
lines changed

Lib/dataclasses.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -623,14 +623,21 @@ def _process_class(cls, repr, eq, order, unsafe_hash, init, frozen):
623623
else:
624624
setattr(cls, f.name, f.default)
625625

626+
# We're inheriting from a frozen dataclass, but we're not frozen.
627+
if cls.__setattr__ is _frozen_setattr and not frozen:
628+
raise TypeError('cannot inherit non-frozen dataclass from a '
629+
'frozen one')
630+
631+
# We're inheriting from a non-frozen dataclass, but we're frozen.
632+
if (hasattr(cls, _MARKER) and cls.__setattr__ is not _frozen_setattr
633+
and frozen):
634+
raise TypeError('cannot inherit frozen dataclass from a '
635+
'non-frozen one')
636+
626637
# Remember all of the fields on our class (including bases). This
627638
# marks this class as being a dataclass.
628639
setattr(cls, _MARKER, fields)
629640

630-
# We also need to check if a parent class is frozen: frozen has to
631-
# be inherited down.
632-
is_frozen = frozen or cls.__setattr__ is _frozen_setattr
633-
634641
# Was this class defined with an explicit __hash__? Note that if
635642
# __eq__ is defined in this class, then python will automatically
636643
# set __hash__ to None. This is a heuristic, as it's possible
@@ -654,7 +661,7 @@ def _process_class(cls, repr, eq, order, unsafe_hash, init, frozen):
654661
if f._field_type in (_FIELD, _FIELD_INITVAR)]
655662
_set_new_attribute(cls, '__init__',
656663
_init_fn(flds,
657-
is_frozen,
664+
frozen,
658665
has_post_init,
659666
# The name to use for the "self" param
660667
# in __init__. Use "self" if possible.
@@ -696,7 +703,7 @@ def _process_class(cls, repr, eq, order, unsafe_hash, init, frozen):
696703
f'in class {cls.__name__}. Consider using '
697704
'functools.total_ordering')
698705

699-
if is_frozen:
706+
if frozen:
700707
for name, fn in [('__setattr__', _frozen_setattr),
701708
('__delattr__', _frozen_delattr)]:
702709
if _set_new_attribute(cls, name, fn):

Lib/test/test_dataclasses.py

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -637,29 +637,6 @@ class C:
637637
y: int
638638
self.assertNotEqual(Point(1, 3), C(1, 3))
639639

640-
def test_frozen(self):
641-
@dataclass(frozen=True)
642-
class C:
643-
i: int
644-
645-
c = C(10)
646-
self.assertEqual(c.i, 10)
647-
with self.assertRaises(FrozenInstanceError):
648-
c.i = 5
649-
self.assertEqual(c.i, 10)
650-
651-
# Check that a derived class is still frozen, even if not
652-
# marked so.
653-
@dataclass
654-
class D(C):
655-
pass
656-
657-
d = D(20)
658-
self.assertEqual(d.i, 20)
659-
with self.assertRaises(FrozenInstanceError):
660-
d.i = 5
661-
self.assertEqual(d.i, 20)
662-
663640
def test_not_tuple(self):
664641
# Test that some of the problems with namedtuple don't happen
665642
# here.
@@ -2475,5 +2452,66 @@ class C(base):
24752452
assert False, f'unknown value for expected={expected!r}'
24762453

24772454

2455+
class TestFrozen(unittest.TestCase):
2456+
def test_frozen(self):
2457+
@dataclass(frozen=True)
2458+
class C:
2459+
i: int
2460+
2461+
c = C(10)
2462+
self.assertEqual(c.i, 10)
2463+
with self.assertRaises(FrozenInstanceError):
2464+
c.i = 5
2465+
self.assertEqual(c.i, 10)
2466+
2467+
def test_inherit(self):
2468+
@dataclass(frozen=True)
2469+
class C:
2470+
i: int
2471+
2472+
@dataclass(frozen=True)
2473+
class D(C):
2474+
j: int
2475+
2476+
d = D(0, 10)
2477+
with self.assertRaises(FrozenInstanceError):
2478+
d.i = 5
2479+
self.assertEqual(d.i, 0)
2480+
2481+
def test_inherit_from_nonfrozen_from_frozen(self):
2482+
@dataclass(frozen=True)
2483+
class C:
2484+
i: int
2485+
2486+
with self.assertRaisesRegex(TypeError,
2487+
'cannot inherit non-frozen dataclass from a frozen one'):
2488+
@dataclass
2489+
class D(C):
2490+
pass
2491+
2492+
def test_inherit_from_frozen_from_nonfrozen(self):
2493+
@dataclass
2494+
class C:
2495+
i: int
2496+
2497+
with self.assertRaisesRegex(TypeError,
2498+
'cannot inherit frozen dataclass from a non-frozen one'):
2499+
@dataclass(frozen=True)
2500+
class D(C):
2501+
pass
2502+
2503+
def test_inherit_from_normal_class(self):
2504+
class C:
2505+
pass
2506+
2507+
@dataclass(frozen=True)
2508+
class D(C):
2509+
i: int
2510+
2511+
d = D(10)
2512+
with self.assertRaises(FrozenInstanceError):
2513+
d.i = 5
2514+
2515+
24782516
if __name__ == '__main__':
24792517
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
For dataclasses, disallow inheriting frozen from non-frozen classes, and
2+
also disallow inheriting non-frozen from frozen classes. This restriction
3+
will be relaxed at a future date.

0 commit comments

Comments
 (0)