|
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