|
27 | 27 | - Private attributes |
28 | 28 | - Deep copying on sealing |
29 | 29 | - Slots |
30 | | -
|
31 | | -Example: |
32 | | - ```python |
33 | | - from dataclasses import dataclass, field |
34 | | - from typing import Optional, List |
35 | | - from libtmux._internal.frozen_dataclass_sealable import ( |
36 | | - frozen_dataclass_sealable, |
37 | | - mutable_during_init, |
38 | | - ) |
39 | | -
|
40 | | - # Mutable base class |
41 | | - @dataclass |
42 | | - class BasePane: |
43 | | - pane_id: str |
44 | | - width: int |
45 | | - height: int |
46 | | -
|
47 | | - def resize(self, width: int, height: int) -> None: |
48 | | - self.width = width |
49 | | - self.height = height |
50 | | -
|
51 | | - # Frozen subclass with field-level mutability |
52 | | - @frozen_dataclass_sealable |
53 | | - class PaneSnapshot(BasePane): |
54 | | - # Regular immutable fields |
55 | | - captured_content: List[str] = field(default_factory=list) |
56 | | -
|
57 | | - # This field can be modified during initialization |
58 | | - parent_window: Optional['WindowSnapshot'] = field( |
59 | | - default=None, |
60 | | - metadata={"mutable_during_init": True} |
61 | | - ) |
62 | | -
|
63 | | - # Override method to block mutation |
64 | | - def resize(self, width: int, height: int) -> None: |
65 | | - raise NotImplementedError("Snapshot is immutable. resize() not allowed.") |
66 | | -
|
67 | | - # Frozen class with circular reference |
68 | | - @frozen_dataclass_sealable |
69 | | - class WindowSnapshot: |
70 | | - window_id: str |
71 | | - name: str |
72 | | -
|
73 | | - # This field can be modified during initialization |
74 | | - panes: List[PaneSnapshot] = field( |
75 | | - default_factory=list, |
76 | | - metadata={"mutable_during_init": True} |
77 | | - ) |
78 | | -
|
79 | | - # Create objects with circular references |
80 | | - window = WindowSnapshot(window_id="win1", name="Main") # Not sealed yet |
81 | | - pane1 = PaneSnapshot(pane_id="1", width=80, height=24) # Not sealed yet |
82 | | - pane2 = PaneSnapshot(pane_id="2", width=80, height=24) # Not sealed yet |
83 | | -
|
84 | | - # Establish circular references |
85 | | - window.panes.append(pane1) |
86 | | - window.panes.append(pane2) |
87 | | - pane1.parent_window = window |
88 | | - pane2.parent_window = window |
89 | | -
|
90 | | - # Seal all objects |
91 | | - window.seal() |
92 | | - pane1.seal() |
93 | | - pane2.seal() |
94 | | -
|
95 | | - # Now all objects are fully immutable |
96 | | - ``` |
97 | | -
|
98 | | -Implementation Notes: |
99 | | - - Uses a custom `__setattr__` to enforce immutability rules |
100 | | - - Internal attributes (starting with '_') can still be modified |
101 | | - - Known limitation: the contents of mutable objects (lists, dicts) can still |
102 | | - be modified even after sealing |
103 | 30 | """ |
104 | 31 |
|
105 | 32 | from __future__ import annotations |
@@ -199,23 +126,49 @@ def is_sealable(cls_or_obj: t.Any) -> bool: |
199 | 126 | """Check if a class or object is sealable. |
200 | 127 |
|
201 | 128 | Args: |
202 | | - cls_or_obj: A class or object to check |
| 129 | + cls_or_obj: The class or object to check |
203 | 130 |
|
204 | | - Returns: |
| 131 | + Returns |
| 132 | + ------- |
205 | 133 | True if the class or object is sealable, False otherwise |
| 134 | + |
| 135 | + Examples: |
| 136 | + >>> from dataclasses import dataclass |
| 137 | + >>> from libtmux._internal.frozen_dataclass_sealable import ( |
| 138 | + ... frozen_dataclass_sealable, is_sealable |
| 139 | + ... ) |
| 140 | + |
| 141 | + >>> # Regular class is not sealable |
| 142 | + >>> @dataclass |
| 143 | + ... class Regular: |
| 144 | + ... value: int |
| 145 | + |
| 146 | + >>> is_sealable(Regular) |
| 147 | + False |
| 148 | + >>> regular = Regular(value=42) |
| 149 | + >>> is_sealable(regular) |
| 150 | + False |
| 151 | + |
| 152 | + >>> # Non-class objects are not sealable |
| 153 | + >>> is_sealable("string") |
| 154 | + False |
| 155 | + >>> is_sealable(42) |
| 156 | + False |
| 157 | + >>> is_sealable(None) |
| 158 | + False |
206 | 159 | """ |
207 | | - # Check if it's a class |
| 160 | + # If it's a class, check if it has a seal method |
208 | 161 | if isinstance(cls_or_obj, type): |
209 | | - return hasattr(cls_or_obj, "seal") and callable(cls_or_obj.seal) |
| 162 | + return hasattr(cls_or_obj, "seal") and callable(getattr(cls_or_obj, "seal")) |
210 | 163 |
|
211 | | - # It's an object instance |
212 | | - return hasattr(cls_or_obj, "seal") and callable(cls_or_obj.seal) |
| 164 | + # If it's an instance, check if it has a seal method |
| 165 | + return hasattr(cls_or_obj, "seal") and callable(getattr(cls_or_obj, "seal")) |
213 | 166 |
|
214 | 167 |
|
215 | 168 | @dataclass_transform(frozen_default=True) |
216 | 169 | def frozen_dataclass_sealable( |
217 | 170 | cls: type | None = None, /, **kwargs: t.Any |
218 | | -) -> t.Any: # mypy doesn't handle complex return types well here |
| 171 | +) -> t.Callable[[type], type] | type: |
219 | 172 | """Create a dataclass that is immutable, with field-level mutability control. |
220 | 173 |
|
221 | 174 | Enhances the standard dataclass with: |
|
0 commit comments