Skip to content

Commit 3517725

Browse files
committed
Revert "feat(frozen_dataclass): implement enhanced frozen_dataclass with post-init mutability and explicit sealing"
This reverts commit 0afc8dd.
1 parent de0dbfd commit 3517725

File tree

1 file changed

+91
-225
lines changed

1 file changed

+91
-225
lines changed

src/libtmux/_internal/frozen_dataclass.py

Lines changed: 91 additions & 225 deletions
Original file line numberDiff line numberDiff line change
@@ -10,281 +10,147 @@
1010
import dataclasses
1111
import functools
1212
import typing as t
13-
from dataclasses import field
1413

1514
from typing_extensions import dataclass_transform
1615

1716
_T = t.TypeVar("_T")
1817

1918

20-
def mutable_during_init(
21-
default: t.Any = None, *, default_factory: t.Callable[[], t.Any] | None = None
22-
) -> dataclasses.Field[t.Any]:
23-
"""Create a field that can be modified during initialization phase.
24-
25-
This field can be modified after __init__ but before the object is sealed.
26-
This is useful for establishing circular references between objects.
27-
28-
Parameters
29-
----------
30-
default : Any, optional
31-
Default value for the field
32-
default_factory : callable, optional
33-
A function that returns the default value
34-
35-
Returns
36-
-------
37-
dataclasses.Field
38-
A field with metadata indicating it's mutable during initialization
39-
40-
Examples
41-
--------
42-
>>> @frozen_dataclass
43-
... class Node:
44-
... name: str
45-
... parent: Node | None = mutable_during_init(None)
46-
... children: list[Node] = mutable_during_init(default_factory=list)
47-
"""
48-
metadata = {"mutable_during_init": True}
49-
result: dataclasses.Field[t.Any]
50-
if default_factory is not None:
51-
result = field(default_factory=default_factory, metadata=metadata)
52-
else:
53-
result = field(default=default, metadata=metadata)
54-
return result
55-
56-
5719
@dataclass_transform(frozen_default=True)
58-
def frozen_dataclass(cls: type[_T] | None = None, *, auto_seal: bool = False) -> t.Any:
59-
"""Create a dataclass that's effectively immutable but with controlled mutability.
20+
def frozen_dataclass(cls: type[_T]) -> type[_T]:
21+
"""Create a dataclass that's effectively immutable but inherits from non-frozen.
6022
6123
This decorator:
6224
1) Applies dataclasses.dataclass(frozen=False) to preserve normal dataclass
6325
generation
64-
2) Identifies fields marked as mutable_during_init via field metadata
65-
3) Allows controlled mutation of those fields after __init__ until sealed
66-
4) Provides a seal() method to make the object completely immutable
67-
5) Tells type-checkers that the resulting class should be treated as frozen
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
6828
6929
Parameters
7030
----------
71-
cls : Type[_T], optional
72-
The class to convert to a controlled-frozen dataclass
73-
auto_seal : bool, default=False
74-
If True, automatically seal the object after __post_init__
75-
If False, the user must call seal() explicitly when done with initialization
31+
cls : Type[_T]
32+
The class to convert to a frozen-like dataclass
7633
7734
Returns
7835
-------
7936
Type[_T]
80-
The processed class with controlled immutability enforced at runtime
37+
The processed class with immutability enforced at runtime
8138
8239
Examples
8340
--------
84-
Basic usage with explicit sealing:
41+
Basic usage:
8542
8643
>>> @frozen_dataclass
8744
... class User:
8845
... id: int
8946
... name: str
90-
... friends: list[str] = mutable_during_init(default_factory=list)
9147
>>> user = User(id=1, name="Alice")
92-
>>> user.friends.append("Bob") # Modifying the list contents (always allowed)
93-
>>> user.friends = ["Bob", "Charlie"] # Allowed for mutable_during_init fields
94-
>>> user.name = "Alice Smith" # Raises error - normal fields can't be modified
48+
>>> user.name
49+
'Alice'
50+
>>> user.name = "Bob"
9551
Traceback (most recent call last):
9652
...
9753
AttributeError: User is immutable: cannot modify field 'name'
98-
>>> user.seal() # Finalize the object, making it completely immutable
99-
>>> user.friends = ["Dave"] # Now raises error after sealing
100-
Traceback (most recent call last):
101-
...
102-
AttributeError: User is sealed: cannot modify any fields
103-
104-
Circular references with explicit sealing:
10554
106-
>>> @frozen_dataclass
107-
... class Node:
108-
... name: str
109-
... parent: 'Node | None' = mutable_during_init(None)
110-
... children: list['Node'] = mutable_during_init(default_factory=list)
111-
>>> root = Node(name="root")
112-
>>> child1 = Node(name="child1")
113-
>>> child2 = Node(name="child2")
114-
>>> # Establish the parent-child relationships
115-
>>> child1.parent = root
116-
>>> child2.parent = root
117-
>>> root.children.append(child1)
118-
>>> root.children.append(child2)
119-
>>> # Once relationships are established, seal all nodes
120-
>>> root.seal()
121-
>>> child1.seal()
122-
>>> child2.seal()
123-
>>> # Now the structure is immutable
124-
>>> child3 = Node(name="child3")
125-
>>> root.children.append(child3) # List contents are still mutable
126-
>>> child3.parent = root # OK - child3 is not sealed yet
127-
>>> try:
128-
... root.name = "ROOT" # Error - root is sealed
129-
... except AttributeError:
130-
... print("Cannot modify sealed object")
131-
Cannot modify sealed object
132-
133-
Auto-sealing after initialization:
55+
Mutating internal attributes (_-prefixed):
13456
135-
>>> @frozen_dataclass(auto_seal=True)
136-
... class Config:
137-
... values: dict[str, str] = field(default_factory=dict)
138-
... def __post_init__(self):
139-
... self.values["timestamp"] = "now"
140-
... # Object will be auto-sealed after __post_init__
141-
>>> conf = Config()
142-
>>> conf.values["key"] = "value" # Dict contents are still mutable
143-
>>> try:
144-
... conf.values = {} # Error - object was auto-sealed
145-
... except AttributeError:
146-
... print("Cannot modify sealed object")
147-
Cannot modify sealed object
57+
>>> user._cache = {"logged_in": True}
58+
>>> user._cache
59+
{'logged_in': True}
14860
149-
Internal attributes are always mutable:
61+
Nested mutable fields limitation:
15062
15163
>>> @frozen_dataclass
152-
... class Cache:
153-
... name: str
154-
>>> cache = Cache(name="mycache")
155-
>>> cache.seal()
156-
>>> cache._internal = {"data": 123} # Private attributes are allowed
157-
>>> cache._internal
158-
{'data': 123}
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'
15994
160-
CAUTION: The implementation allows setting _sealed=False to bypass immutability:
95+
Security consideration - modifying the _frozen flag:
16196
16297
>>> @frozen_dataclass
16398
... class SecureData:
16499
... secret: str
165100
>>> data = SecureData(secret="password123")
166-
>>> data.seal()
167-
>>> # CAUTION: This bypasses the immutability protection
168-
>>> data._frozen = False # Intentionally bypassing immutability
169-
>>> data.secret = "hacked" # Works because object is no longer sealed
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
170109
>>> data.secret
171110
'hacked'
172111
"""
173-
174-
def wrap(cls: type[_T]) -> type[_T]:
175-
# A. Convert to a dataclass with frozen=False
176-
cls = dataclasses.dataclass(cls)
177-
178-
# B. Collect fields marked as mutable during initialization
179-
mutable_during_init_fields = set()
180-
for name, field_obj in getattr(cls, "__dataclass_fields__", {}).items():
181-
if field_obj.metadata.get("mutable_during_init", False):
182-
mutable_during_init_fields.add(name)
183-
184-
# Store on class for runtime checks
185-
setattr(cls, "_mutable_during_init_fields", mutable_during_init_fields)
186-
187-
# C. Add state tracking attributes
188-
cls.__annotations__["_initializing"] = bool
189-
cls.__annotations__["_sealed"] = bool
190-
cls.__annotations__["_frozen"] = bool # For backward compatibility
191-
setattr(cls, "_initializing", True)
192-
setattr(cls, "_sealed", False)
193-
setattr(cls, "_frozen", True) # Backward compatibility
194-
195-
# Save original methods
196-
original_init = cls.__init__
197-
original_post_init = getattr(cls, "__post_init__", None)
198-
199-
# D. Custom __init__ to set initial state
200-
@functools.wraps(original_init)
201-
def __init__(self: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
202-
# Set initializing state
203-
object.__setattr__(self, "_initializing", True)
204-
object.__setattr__(self, "_sealed", False)
205-
object.__setattr__(self, "_frozen", False)
206-
207-
# Call original __init__
208-
original_init(self, *args, **kwargs)
209-
210-
# After __init__, only mutable_during_init fields can be changed
211-
object.__setattr__(self, "_initializing", False)
212-
object.__setattr__(self, "_frozen", True)
213-
214-
# If there's a post_init, it will be called separately
215-
216-
# E. Custom __post_init__ to potentially auto-seal
217-
def __post_init__(self: t.Any) -> None:
218-
if original_post_init:
219-
original_post_init(self)
220-
221-
if auto_seal:
222-
self.seal()
223-
224-
# F. Add seal method
225-
def seal(self: t.Any) -> None:
226-
"""Finalize this object, making it completely immutable."""
227-
object.__setattr__(self, "_sealed", True)
228-
229-
# G. Custom attribute setting to enforce rules
230-
def __setattr__(self: t.Any, name: str, value: t.Any) -> None:
231-
# Allow setting during initialization phase
232-
if getattr(self, "_initializing", True):
233-
object.__setattr__(self, name, value)
234-
return
235-
236-
# Backward compatibility: if _frozen was explicitly set to False,
237-
# allow mutations
238-
if (
239-
name not in ("_frozen", "_sealed")
240-
and getattr(self, "_frozen", True) is False
241-
):
242-
object.__setattr__(self, name, value)
243-
return
244-
245-
# Always allow internal attributes
246-
if name.startswith("_"):
247-
object.__setattr__(self, name, value)
248-
return
249-
250-
# If sealed, no attributes can be modified
251-
if getattr(self, "_sealed", False):
252-
error_msg = f"{cls.__name__} is sealed: cannot modify any fields"
253-
raise AttributeError(error_msg)
254-
255-
# If not sealed but past init, only specially marked fields can be changed
256-
if name in getattr(self, "_mutable_during_init_fields", set()):
257-
object.__setattr__(self, name, value)
258-
return
259-
260-
# Otherwise, field cannot be modified
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
261135
error_msg = f"{cls.__name__} is immutable: cannot modify field '{name}'"
262136
raise AttributeError(error_msg)
263137

264-
# H. Custom attribute deletion to prevent deletion
265-
def __delattr__(self: t.Any, name: str) -> None:
266-
if name.startswith("_") and not getattr(self, "_sealed", False):
267-
object.__delattr__(self, name)
268-
return
269-
270-
if getattr(self, "_sealed", False):
271-
error_msg = f"{cls.__name__} is sealed: cannot delete any fields"
272-
raise AttributeError(error_msg)
138+
# Allow the assignment
139+
object.__setattr__(self, name, value)
273140

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):
274145
error_msg = f"{cls.__name__} is immutable: cannot delete field '{name}'"
275146
raise AttributeError(error_msg)
276147

277-
# I. Inject methods
278-
setattr(cls, "__init__", __init__)
279-
setattr(cls, "__post_init__", __post_init__)
280-
setattr(cls, "seal", seal)
281-
setattr(cls, "__setattr__", __setattr__)
282-
setattr(cls, "__delattr__", __delattr__)
283-
284-
return cls
148+
# Allow the deletion
149+
object.__delattr__(self, name)
285150

286-
# Support both @frozen_dataclass and @frozen_dataclass(...) syntax
287-
if cls is None:
288-
return wrap
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
289155

290-
return wrap(cls)
156+
return cls

0 commit comments

Comments
 (0)