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.
This commit is contained in:
Eric V. Smith 2018-02-26 20:38:33 -05:00 committed by GitHub
parent 72d9b2be36
commit 2fa6b9eae0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 77 additions and 29 deletions

View File

@ -623,14 +623,21 @@ def _process_class(cls, repr, eq, order, unsafe_hash, init, frozen):
else: else:
setattr(cls, f.name, f.default) setattr(cls, f.name, f.default)
# We're inheriting from a frozen dataclass, but we're not frozen.
if cls.__setattr__ is _frozen_setattr and not frozen:
raise TypeError('cannot inherit non-frozen dataclass from a '
'frozen one')
# We're inheriting from a non-frozen dataclass, but we're frozen.
if (hasattr(cls, _MARKER) and cls.__setattr__ is not _frozen_setattr
and frozen):
raise TypeError('cannot inherit frozen dataclass from a '
'non-frozen one')
# Remember all of the fields on our class (including bases). This # Remember all of the fields on our class (including bases). This
# marks this class as being a dataclass. # marks this class as being a dataclass.
setattr(cls, _MARKER, fields) setattr(cls, _MARKER, fields)
# We also need to check if a parent class is frozen: frozen has to
# be inherited down.
is_frozen = frozen or cls.__setattr__ is _frozen_setattr
# Was this class defined with an explicit __hash__? Note that if # Was this class defined with an explicit __hash__? Note that if
# __eq__ is defined in this class, then python will automatically # __eq__ is defined in this class, then python will automatically
# set __hash__ to None. This is a heuristic, as it's possible # 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):
if f._field_type in (_FIELD, _FIELD_INITVAR)] if f._field_type in (_FIELD, _FIELD_INITVAR)]
_set_new_attribute(cls, '__init__', _set_new_attribute(cls, '__init__',
_init_fn(flds, _init_fn(flds,
is_frozen, frozen,
has_post_init, has_post_init,
# The name to use for the "self" param # The name to use for the "self" param
# in __init__. Use "self" if possible. # in __init__. Use "self" if possible.
@ -696,7 +703,7 @@ def _process_class(cls, repr, eq, order, unsafe_hash, init, frozen):
f'in class {cls.__name__}. Consider using ' f'in class {cls.__name__}. Consider using '
'functools.total_ordering') 'functools.total_ordering')
if is_frozen: if frozen:
for name, fn in [('__setattr__', _frozen_setattr), for name, fn in [('__setattr__', _frozen_setattr),
('__delattr__', _frozen_delattr)]: ('__delattr__', _frozen_delattr)]:
if _set_new_attribute(cls, name, fn): if _set_new_attribute(cls, name, fn):

View File

@ -637,29 +637,6 @@ class TestCase(unittest.TestCase):
y: int y: int
self.assertNotEqual(Point(1, 3), C(1, 3)) self.assertNotEqual(Point(1, 3), C(1, 3))
def test_frozen(self):
@dataclass(frozen=True)
class C:
i: int
c = C(10)
self.assertEqual(c.i, 10)
with self.assertRaises(FrozenInstanceError):
c.i = 5
self.assertEqual(c.i, 10)
# Check that a derived class is still frozen, even if not
# marked so.
@dataclass
class D(C):
pass
d = D(20)
self.assertEqual(d.i, 20)
with self.assertRaises(FrozenInstanceError):
d.i = 5
self.assertEqual(d.i, 20)
def test_not_tuple(self): def test_not_tuple(self):
# Test that some of the problems with namedtuple don't happen # Test that some of the problems with namedtuple don't happen
# here. # here.
@ -2475,5 +2452,66 @@ class TestHash(unittest.TestCase):
assert False, f'unknown value for expected={expected!r}' assert False, f'unknown value for expected={expected!r}'
class TestFrozen(unittest.TestCase):
def test_frozen(self):
@dataclass(frozen=True)
class C:
i: int
c = C(10)
self.assertEqual(c.i, 10)
with self.assertRaises(FrozenInstanceError):
c.i = 5
self.assertEqual(c.i, 10)
def test_inherit(self):
@dataclass(frozen=True)
class C:
i: int
@dataclass(frozen=True)
class D(C):
j: int
d = D(0, 10)
with self.assertRaises(FrozenInstanceError):
d.i = 5
self.assertEqual(d.i, 0)
def test_inherit_from_nonfrozen_from_frozen(self):
@dataclass(frozen=True)
class C:
i: int
with self.assertRaisesRegex(TypeError,
'cannot inherit non-frozen dataclass from a frozen one'):
@dataclass
class D(C):
pass
def test_inherit_from_frozen_from_nonfrozen(self):
@dataclass
class C:
i: int
with self.assertRaisesRegex(TypeError,
'cannot inherit frozen dataclass from a non-frozen one'):
@dataclass(frozen=True)
class D(C):
pass
def test_inherit_from_normal_class(self):
class C:
pass
@dataclass(frozen=True)
class D(C):
i: int
d = D(10)
with self.assertRaises(FrozenInstanceError):
d.i = 5
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -0,0 +1,3 @@
For dataclasses, disallow inheriting frozen from non-frozen classes, and
also disallow inheriting non-frozen from frozen classes. This restriction
will be relaxed at a future date.