Skip to content

Commit 2e8a0cd

Browse files
Copilotpancettabrownbaerchen
authored
Fix FrozenClass attribute isolation and linting (#605)
* Initial plan * Fix FrozenClass to use per-class attrs instead of shared Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> * Add missing return statement in __getattr__ Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> * Add success message to test __main__ block Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> * Fix black formatting: remove trailing blank line Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> * Update pySDC/tests/test_helpers/test_frozen_class.py Co-authored-by: Thomas Saupe <39156931+brownbaerchen@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pancetta <7158893+pancetta@users.noreply.github.com> Co-authored-by: Robert Speck <pancetta@users.noreply.github.com> Co-authored-by: Thomas Saupe <39156931+brownbaerchen@users.noreply.github.com>
1 parent 2bf42c8 commit 2e8a0cd

File tree

2 files changed

+57
-7
lines changed

2 files changed

+57
-7
lines changed

pySDC/helpers/pysdc_helper.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@ class FrozenClass(object):
99
__isfrozen: Flag to freeze a class
1010
"""
1111

12-
attrs = []
13-
1412
__isfrozen = False
1513

14+
def __init_subclass__(cls, **kwargs):
15+
"""
16+
Called when a class inherits from FrozenClass.
17+
Creates a separate attrs list for each subclass.
18+
"""
19+
super().__init_subclass__(**kwargs)
20+
# Create a new attrs list for this specific subclass
21+
cls.attrs = []
22+
1623
def __setattr__(self, key, value):
1724
"""
1825
Function called when setting attributes
@@ -23,7 +30,7 @@ def __setattr__(self, key, value):
2330
"""
2431

2532
# check if attribute exists and if class is frozen
26-
if self.__isfrozen and not (key in self.attrs or hasattr(self, key)):
33+
if self.__isfrozen and not (key in type(self).attrs or hasattr(self, key)):
2734
raise TypeError(f'{type(self).__name__!r} is a frozen class, cannot add attribute {key!r}')
2835

2936
object.__setattr__(self, key, value)
@@ -32,10 +39,10 @@ def __getattr__(self, key):
3239
"""
3340
This is needed in case the variables have not been initialized after adding.
3441
"""
35-
if key in self.attrs:
42+
if key in type(self).attrs:
3643
return None
3744
else:
38-
super().__getattribute__(key)
45+
return super().__getattribute__(key)
3946

4047
@classmethod
4148
def add_attr(cls, key, raise_error_if_exists=False):
@@ -79,4 +86,4 @@ def __dir__(self):
7986
"""
8087
My hope is that some editors can use this for dynamic autocompletion.
8188
"""
82-
return super().__dir__() + self.attrs
89+
return super().__dir__() + type(self).attrs

pySDC/tests/test_helpers/test_frozen_class.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,51 @@ class Dummy2(FrozenClass):
2929
Dummy2.add_attr('bar')
3030
you = Dummy2()
3131
you.bar = 5
32-
assert me.bar is None, 'Attribute was set across classes'
32+
# With the fix, 'bar' should NOT be accessible on Dummy instances at all
33+
# because it was only added to Dummy2, not Dummy
34+
with pytest.raises(AttributeError): # bar is not an attribute of Dummy
35+
_ = me.bar
36+
37+
38+
@pytest.mark.base
39+
def test_frozen_class_isolation():
40+
"""
41+
Test that attributes added to one frozen class don't leak to other frozen classes.
42+
This specifically tests the fix for the issue where attrs was shared across all subclasses.
43+
"""
44+
from pySDC.helpers.pysdc_helper import FrozenClass
45+
46+
# Simulate Level and Step status classes from the issue
47+
class LevelStatus(FrozenClass):
48+
def __init__(self):
49+
self.residual = None
50+
self._freeze()
51+
52+
class StepStatus(FrozenClass):
53+
def __init__(self):
54+
self.iter = None
55+
self._freeze()
56+
57+
# Add attribute to LevelStatus only
58+
LevelStatus.add_attr('error_embedded_estimate')
59+
60+
# Verify attrs are separate
61+
assert 'error_embedded_estimate' in LevelStatus.attrs
62+
assert 'error_embedded_estimate' not in StepStatus.attrs
63+
64+
level_status = LevelStatus()
65+
step_status = StepStatus()
66+
67+
# Should work - error_embedded_estimate was added to LevelStatus
68+
level_status.error_embedded_estimate = 0.01
69+
assert level_status.error_embedded_estimate == 0.01
70+
71+
# Should fail - error_embedded_estimate was NOT added to StepStatus
72+
with pytest.raises(TypeError, match="is a frozen class, cannot add attribute"):
73+
step_status.error_embedded_estimate = 0.02
3374

3475

3576
if __name__ == '__main__':
3677
test_frozen_class()
78+
test_frozen_class_isolation()
79+
print("All tests passed!")

0 commit comments

Comments
 (0)