summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/dataclasses.py10
-rw-r--r--Lib/test/test_dataclasses.py44
-rw-r--r--Misc/NEWS.d/next/Library/2023-03-10-13-21-16.gh-issue-102578.-gujoI.rst4
3 files changed, 52 insertions, 6 deletions
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index 8bc8594..78a126f 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -616,21 +616,19 @@ def _repr_fn(fields, globals):
def _frozen_get_del_attr(cls, fields, globals):
locals = {'cls': cls,
'FrozenInstanceError': FrozenInstanceError}
+ condition = 'type(self) is cls'
if fields:
- fields_str = '(' + ','.join(repr(f.name) for f in fields) + ',)'
- else:
- # Special case for the zero-length tuple.
- fields_str = '()'
+ condition += ' or name in {' + ', '.join(repr(f.name) for f in fields) + '}'
return (_create_fn('__setattr__',
('self', 'name', 'value'),
- (f'if type(self) is cls or name in {fields_str}:',
+ (f'if {condition}:',
' 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 {condition}:',
' raise FrozenInstanceError(f"cannot delete field {name!r}")',
f'super(cls, self).__delattr__(name)'),
locals=locals,
diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py
index 589f229..5486b2e 100644
--- a/Lib/test/test_dataclasses.py
+++ b/Lib/test/test_dataclasses.py
@@ -2767,6 +2767,19 @@ class TestFrozen(unittest.TestCase):
c.i = 5
self.assertEqual(c.i, 10)
+ def test_frozen_empty(self):
+ @dataclass(frozen=True)
+ class C:
+ pass
+
+ c = C()
+ self.assertFalse(hasattr(c, 'i'))
+ with self.assertRaises(FrozenInstanceError):
+ c.i = 5
+ self.assertFalse(hasattr(c, 'i'))
+ with self.assertRaises(FrozenInstanceError):
+ del c.i
+
def test_inherit(self):
@dataclass(frozen=True)
class C:
@@ -2890,6 +2903,37 @@ class TestFrozen(unittest.TestCase):
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:
+ pass
+
+ class S(D):
+ pass
+
+ s = S()
+ self.assertFalse(hasattr(s, 'x'))
+ s.x = 5
+ self.assertEqual(s.x, 5)
+
+ del s.x
+ self.assertFalse(hasattr(s, 'x'))
+ with self.assertRaises(AttributeError) as cm:
+ del s.x
+ self.assertNotIsInstance(cm.exception, FrozenInstanceError)
+
def test_overwriting_frozen(self):
# frozen uses __setattr__ and __delattr__.
with self.assertRaisesRegex(TypeError,
diff --git a/Misc/NEWS.d/next/Library/2023-03-10-13-21-16.gh-issue-102578.-gujoI.rst b/Misc/NEWS.d/next/Library/2023-03-10-13-21-16.gh-issue-102578.-gujoI.rst
new file mode 100644
index 0000000..7307148
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-03-10-13-21-16.gh-issue-102578.-gujoI.rst
@@ -0,0 +1,4 @@
+Speed up setting or deleting mutable attributes on non-dataclass subclasses of
+frozen dataclasses. Due to the implementation of ``__setattr__`` and
+``__delattr__`` for frozen dataclasses, this previously had a time complexity
+of ``O(n)``. It now has a time complexity of ``O(1)``.