Skip to content

Commit d4dc6e7

Browse files
committedFeb 28, 2025
frozen_dataclass(style[documentation]): Fix line length violations in comments
why: Ensure codebase fully complies with ruff linting rules and maintains consistent style what: - Break long security warning comment into multiple lines in frozen_dataclass docstring - Reorganize comment text for better readability while preserving meaning - Split error description comments in test_frozen_dataclass.py into two lines each - Maintain consistent indentation and formatting in multi-line comments See also: The style changes maintain PEP8-inspired 88 character limit
1 parent 372fa2f commit d4dc6e7

File tree

2 files changed

+584
-0
lines changed

2 files changed

+584
-0
lines changed
 
+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""Custom frozen dataclass implementation that works with inheritance.
2+
3+
This module provides a `frozen_dataclass` decorator that allows creating
4+
effectively immutable dataclasses that can inherit from mutable ones,
5+
which is not possible with standard dataclasses.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import dataclasses
11+
import functools
12+
import typing as t
13+
14+
from typing_extensions import dataclass_transform
15+
16+
_T = t.TypeVar("_T")
17+
18+
19+
@dataclass_transform(frozen_default=True)
20+
def frozen_dataclass(cls: type[_T]) -> type[_T]:
21+
"""Create a dataclass that's effectively immutable but inherits from non-frozen.
22+
23+
This decorator:
24+
1) Applies dataclasses.dataclass(frozen=False) to preserve normal dataclass
25+
generation
26+
2) Overrides __setattr__ and __delattr__ to block changes post-init
27+
3) Tells type-checkers that the resulting class should be treated as frozen
28+
29+
Parameters
30+
----------
31+
cls : Type[_T]
32+
The class to convert to a frozen-like dataclass
33+
34+
Returns
35+
-------
36+
Type[_T]
37+
The processed class with immutability enforced at runtime
38+
39+
Examples
40+
--------
41+
Basic usage:
42+
43+
>>> @frozen_dataclass
44+
... class User:
45+
... id: int
46+
... name: str
47+
>>> user = User(id=1, name="Alice")
48+
>>> user.name
49+
'Alice'
50+
>>> user.name = "Bob"
51+
Traceback (most recent call last):
52+
...
53+
AttributeError: User is immutable: cannot modify field 'name'
54+
55+
Mutating internal attributes (_-prefixed):
56+
57+
>>> user._cache = {"logged_in": True}
58+
>>> user._cache
59+
{'logged_in': True}
60+
61+
Nested mutable fields limitation:
62+
63+
>>> @frozen_dataclass
64+
... class Container:
65+
... items: list[int]
66+
>>> c = Container(items=[1, 2])
67+
>>> c.items.append(3) # allowed; mutable field itself isn't protected
68+
>>> c.items
69+
[1, 2, 3]
70+
>>> # For deep immutability, use immutable collections (tuple, frozenset)
71+
>>> @frozen_dataclass
72+
... class ImmutableContainer:
73+
... items: tuple[int, ...] = (1, 2)
74+
>>> ic = ImmutableContainer()
75+
>>> ic.items
76+
(1, 2)
77+
78+
Inheritance from mutable base classes:
79+
80+
>>> import dataclasses
81+
>>> @dataclasses.dataclass
82+
... class MutableBase:
83+
... value: int
84+
>>> @frozen_dataclass
85+
... class ImmutableSub(MutableBase):
86+
... pass
87+
>>> obj = ImmutableSub(42)
88+
>>> obj.value
89+
42
90+
>>> obj.value = 100
91+
Traceback (most recent call last):
92+
...
93+
AttributeError: ImmutableSub is immutable: cannot modify field 'value'
94+
95+
Security consideration - modifying the _frozen flag:
96+
97+
>>> @frozen_dataclass
98+
... class SecureData:
99+
... secret: str
100+
>>> data = SecureData(secret="password123")
101+
>>> data.secret = "hacked"
102+
Traceback (most recent call last):
103+
...
104+
AttributeError: SecureData is immutable: cannot modify field 'secret'
105+
>>> # CAUTION: The _frozen attribute can be modified to bypass immutability
106+
>>> # protection. This is a known limitation of this implementation
107+
>>> data._frozen = False # intentionally bypassing immutability
108+
>>> data.secret = "hacked" # now works because object is no longer frozen
109+
>>> data.secret
110+
'hacked'
111+
"""
112+
# A. Convert to a dataclass with frozen=False
113+
cls = dataclasses.dataclass(cls)
114+
115+
# B. Explicitly annotate and initialize the `_frozen` attribute for static analysis
116+
cls.__annotations__["_frozen"] = bool
117+
setattr(cls, "_frozen", False)
118+
119+
# Save the original __init__ to use in our hooks
120+
original_init = cls.__init__
121+
122+
# C. Create a new __init__ that will call the original and then set _frozen flag
123+
@functools.wraps(original_init)
124+
def __init__(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
125+
# Call the original __init__
126+
original_init(self, *args, **kwargs)
127+
# Set the _frozen flag to make object immutable
128+
object.__setattr__(self, "_frozen", True)
129+
130+
# D. Custom attribute assignment method
131+
def __setattr__(self: t.Any, name: str, value: t.Any) -> None:
132+
# If _frozen is set and we're trying to set a field, block it
133+
if getattr(self, "_frozen", False) and not name.startswith("_"):
134+
# Allow mutation of private (_-prefixed) attributes after initialization
135+
error_msg = f"{cls.__name__} is immutable: cannot modify field '{name}'"
136+
raise AttributeError(error_msg)
137+
138+
# Allow the assignment
139+
object.__setattr__(self, name, value)
140+
141+
# E. Custom attribute deletion method
142+
def __delattr__(self: t.Any, name: str) -> None:
143+
# If we're frozen, block deletion
144+
if getattr(self, "_frozen", False):
145+
error_msg = f"{cls.__name__} is immutable: cannot delete field '{name}'"
146+
raise AttributeError(error_msg)
147+
148+
# Allow the deletion
149+
object.__delattr__(self, name)
150+
151+
# F. Inject methods into the class (using setattr to satisfy mypy)
152+
setattr(cls, "__init__", __init__) # Sets _frozen flag post-initialization
153+
setattr(cls, "__setattr__", __setattr__) # Blocks attribute modification post-init
154+
setattr(cls, "__delattr__", __delattr__) # Blocks attribute deletion post-init
155+
156+
return cls

0 commit comments

Comments
 (0)