Coverage for src/dataknobs_fsm/core/data_modes.py: 47%
103 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:46 -0600
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-20 16:46 -0600
1"""Data mode system for FSM state management.
3This module defines the data handling strategies for state instances:
4- COPY: Deep copy on state entry, isolated modifications
5- REFERENCE: Lazy loading with optimistic locking
6- DIRECT: In-place modifications (single-threaded only)
7"""
9from abc import ABC, abstractmethod
10from copy import deepcopy
11from enum import Enum
12from threading import Lock
13from typing import Any, Dict, TypeVar
15T = TypeVar("T")
18class DataHandlingMode(Enum):
19 """Data handling modes for state instances - defines how data is managed within states."""
21 COPY = "copy" # Deep copy on entry, isolated changes
22 REFERENCE = "reference" # Lazy loading with locking
23 DIRECT = "direct" # In-place modifications
26class DataHandler(ABC):
27 """Abstract base class for data mode handlers."""
29 def __init__(self, mode: DataHandlingMode):
30 """Initialize the data handler.
32 Args:
33 mode: The data mode this handler implements.
34 """
35 self.mode = mode
37 @abstractmethod
38 def on_entry(self, data: Any) -> Any:
39 """Handle data when entering a state.
41 Args:
42 data: The input data to the state.
44 Returns:
45 The data to be used within the state.
46 """
47 pass
49 @abstractmethod
50 def on_modification(self, data: Any) -> Any:
51 """Handle data modification within a state.
53 Args:
54 data: The data being modified.
56 Returns:
57 The modified data.
58 """
59 pass
61 @abstractmethod
62 def on_exit(self, data: Any, commit: bool = True) -> Any:
63 """Handle data when exiting a state.
65 Args:
66 data: The state's data.
67 commit: Whether to commit changes.
69 Returns:
70 The final data after exit processing.
71 """
72 pass
74 @abstractmethod
75 def supports_concurrent_access(self) -> bool:
76 """Check if this handler supports concurrent access.
78 Returns:
79 True if concurrent access is supported.
80 """
81 pass
84class CopyModeHandler(DataHandler):
85 """Handler for COPY mode - deep copy with isolated modifications."""
87 def __init__(self):
88 """Initialize the COPY mode handler."""
89 super().__init__(DataHandlingMode.COPY)
90 self._originals: Dict[int, Any] = {}
91 self._lock = Lock()
93 def on_entry(self, data: Any) -> Any:
94 """Create a deep copy of the data on state entry.
96 Args:
97 data: The input data to copy.
99 Returns:
100 A deep copy of the data.
101 """
102 copied = deepcopy(data)
103 with self._lock:
104 self._originals[id(copied)] = data
105 return copied
107 def on_modification(self, data: Any) -> Any:
108 """Allow modifications to the copied data.
110 Args:
111 data: The copied data being modified.
113 Returns:
114 The same data (modifications are isolated).
115 """
116 return data
118 def on_exit(self, data: Any, commit: bool = True) -> Any:
119 """Handle exit, optionally committing changes.
121 Args:
122 data: The modified copy.
123 commit: Whether to return modified or original data.
125 Returns:
126 Modified data if commit=True, original otherwise.
127 """
128 with self._lock:
129 original = self._originals.pop(id(data), None)
131 if commit:
132 return data
133 else:
134 return original if original is not None else data
136 def supports_concurrent_access(self) -> bool:
137 """COPY mode supports concurrent access.
139 Returns:
140 True, as each state gets its own copy.
141 """
142 return True
145class ReferenceModeHandler(DataHandler):
146 """Handler for REFERENCE mode - lazy loading with optimistic locking."""
148 def __init__(self):
149 """Initialize the REFERENCE mode handler."""
150 super().__init__(DataHandlingMode.REFERENCE)
151 # Don't use WeakValueDictionary as it can't hold dict objects
152 self._references: Dict[int, Any] = {}
153 self._versions: Dict[int, int] = {}
154 self._locks: Dict[int, Lock] = {}
155 self._global_lock = Lock()
157 def on_entry(self, data: Any) -> Any:
158 """Store a reference to the data with versioning.
160 Args:
161 data: The input data to reference.
163 Returns:
164 The same data object (by reference).
165 """
166 data_id = id(data)
167 with self._global_lock:
168 if data_id not in self._references:
169 self._references[data_id] = data
170 self._versions[data_id] = 0
171 self._locks[data_id] = Lock()
172 return data
174 def on_modification(self, data: Any) -> Any:
175 """Track modifications with optimistic locking.
177 Args:
178 data: The data being modified.
180 Returns:
181 The same data object.
182 """
183 data_id = id(data)
184 with self._global_lock:
185 if data_id in self._locks:
186 lock = self._locks[data_id]
187 else:
188 lock = Lock()
189 self._locks[data_id] = lock
191 with lock:
192 if data_id in self._versions:
193 self._versions[data_id] += 1
195 return data
197 def on_exit(self, data: Any, commit: bool = True) -> Any:
198 """Clean up references on exit.
200 Args:
201 data: The referenced data.
202 commit: Whether changes should be kept.
204 Returns:
205 The data object.
206 """
207 data_id = id(data)
209 if not commit:
210 # In REFERENCE mode, we can't truly rollback in-place changes
211 # This would require snapshot/restore functionality
212 pass
214 # Clean up tracking if no longer needed
215 with self._global_lock:
216 # Clean up if version is 0 (no modifications)
217 if data_id in self._versions and self._versions[data_id] == 0:
218 self._references.pop(data_id, None)
219 self._versions.pop(data_id, None)
220 self._locks.pop(data_id, None)
222 return data
224 def supports_concurrent_access(self) -> bool:
225 """REFERENCE mode supports concurrent access with locking.
227 Returns:
228 True, with optimistic locking for safety.
229 """
230 return True
233class DirectModeHandler(DataHandler):
234 """Handler for DIRECT mode - in-place modifications without copying."""
236 def __init__(self):
237 """Initialize the DIRECT mode handler."""
238 super().__init__(DataHandlingMode.DIRECT)
239 self._active_data: Any | None = None
240 self._lock = Lock()
242 def on_entry(self, data: Any) -> Any:
243 """Use data directly without copying.
245 Args:
246 data: The input data to use directly.
248 Returns:
249 The same data object.
250 """
251 with self._lock:
252 if self._active_data is not None:
253 raise RuntimeError(
254 "DIRECT mode does not support concurrent access. "
255 "Another state is currently using direct mode."
256 )
257 self._active_data = data
258 return data
260 def on_modification(self, data: Any) -> Any:
261 """Allow direct in-place modifications.
263 Args:
264 data: The data being modified.
266 Returns:
267 The same data object.
268 """
269 return data
271 def on_exit(self, data: Any, commit: bool = True) -> Any:
272 """Clear the active data reference.
274 Args:
275 data: The data object.
276 commit: Whether to keep changes (always True for DIRECT).
278 Returns:
279 The data object.
280 """
281 with self._lock:
282 self._active_data = None
284 # In DIRECT mode, changes are always committed (in-place)
285 return data
287 def supports_concurrent_access(self) -> bool:
288 """DIRECT mode does not support concurrent access.
290 Returns:
291 False, as only one state can use DIRECT mode at a time.
292 """
293 return False
296class DataModeManager:
297 """Manager for data mode handlers."""
299 def __init__(self, default_mode: DataHandlingMode = DataHandlingMode.COPY):
300 """Initialize the data mode manager.
302 Args:
303 default_mode: The default data mode to use.
304 """
305 self.default_mode = default_mode
306 self._handlers: Dict[DataHandlingMode, DataHandler] = {
307 DataHandlingMode.COPY: CopyModeHandler(),
308 DataHandlingMode.REFERENCE: ReferenceModeHandler(),
309 DataHandlingMode.DIRECT: DirectModeHandler(),
310 }
312 def get_handler(self, mode: DataHandlingMode | None = None) -> DataHandler:
313 """Get a handler for the specified mode.
315 Args:
316 mode: The data mode, or None for default.
318 Returns:
319 The appropriate data handler.
320 """
321 if mode is None:
322 mode = self.default_mode
323 return self._handlers[mode]
325 def set_default_mode(self, mode: DataHandlingMode) -> None:
326 """Set the default data mode.
328 Args:
329 mode: The new default data mode.
330 """
331 self.default_mode = mode
334# Global registry of data handlers
335_GLOBAL_HANDLERS = {
336 DataHandlingMode.COPY: CopyModeHandler(),
337 DataHandlingMode.REFERENCE: ReferenceModeHandler(),
338 DataHandlingMode.DIRECT: DirectModeHandler(),
339}
342def get_data_handler(mode: DataHandlingMode) -> DataHandler:
343 """Get a data handler for the specified mode.
345 Args:
346 mode: The data mode.
348 Returns:
349 The appropriate data handler.
350 """
351 return _GLOBAL_HANDLERS[mode]