Coverage for src/dataknobs_fsm/core/data_wrapper.py: 40%
117 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 wrapper for FSM that provides a consistent interface for data access.
3This module implements a hybrid solution for data handling in the FSM:
4- User functions receive raw dict data by default (simple, predictable)
5- Optionally, users can work with FSMData wrapper for enhanced functionality
6- Internal FSM operations use the wrapper for consistency
7"""
9from typing import Any, Dict, Union, Iterator, KeysView, ValuesView, ItemsView
10from collections.abc import MutableMapping
11import copy
14class FSMData(MutableMapping):
15 """A data wrapper that supports both dict-style and attribute access.
17 This class provides:
18 1. Dict-style access: data['key']
19 2. Attribute access: data.key
20 3. Compatibility with existing functions expecting either pattern
21 4. Transparent conversion to/from dict
23 The FSM internally uses this wrapper but always passes raw dict data
24 to user functions unless they explicitly request the wrapper.
25 """
27 # Explicitly mark as unhashable (mutable mapping)
28 __hash__ = None # type: ignore[assignment]
30 def __init__(self, data: Dict[str, Any] | None = None):
31 """Initialize FSMData wrapper.
33 Args:
34 data: Initial data dictionary. Defaults to empty dict.
35 """
36 # Store data in __dict__ to avoid recursion with __getattr__
37 object.__setattr__(self, '_data', data if data is not None else {})
39 # Dict-style access methods
40 def __getitem__(self, key: str) -> Any:
41 """Get item using dict-style access."""
42 return self._data[key]
44 def __setitem__(self, key: str, value: Any) -> None:
45 """Set item using dict-style access."""
46 self._data[key] = value
48 def __delitem__(self, key: str) -> None:
49 """Delete item using dict-style access."""
50 del self._data[key]
52 def __contains__(self, key: str) -> bool:
53 """Check if key exists in data."""
54 return key in self._data
56 def __iter__(self) -> Iterator[str]:
57 """Iterate over keys."""
58 return iter(self._data)
60 def __len__(self) -> int:
61 """Get number of items."""
62 return len(self._data)
64 # Attribute-style access methods
65 def __getattr__(self, name: str) -> Any:
66 """Get attribute using dot notation."""
67 if name.startswith('_'):
68 # Don't intercept private attributes
69 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
70 try:
71 return self._data[name]
72 except KeyError:
73 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") from None
75 def __setattr__(self, name: str, value: Any) -> None:
76 """Set attribute using dot notation."""
77 if name.startswith('_'):
78 # Store private attributes normally
79 object.__setattr__(self, name, value)
80 else:
81 self._data[name] = value
83 def __delattr__(self, name: str) -> None:
84 """Delete attribute using dot notation."""
85 if name.startswith('_'):
86 object.__delattr__(self, name)
87 else:
88 try:
89 del self._data[name]
90 except KeyError:
91 raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") from None
93 # Dict-like methods
94 def get(self, key: str, default: Any = None) -> Any:
95 """Get value with default."""
96 return self._data.get(key, default)
98 def keys(self) -> KeysView[str]:
99 """Get keys view."""
100 return self._data.keys()
102 def values(self) -> ValuesView[Any]:
103 """Get values view."""
104 return self._data.values()
106 def items(self) -> ItemsView[str, Any]:
107 """Get items view."""
108 return self._data.items()
110 def update(self, other: Union[Dict[str, Any], 'FSMData'] = None, **kwargs) -> None:
111 """Update data from dict or another FSMData."""
112 if other is not None:
113 if isinstance(other, FSMData):
114 self._data.update(other._data)
115 else:
116 self._data.update(other)
117 self._data.update(kwargs)
119 def clear(self) -> None:
120 """Clear all data."""
121 self._data.clear()
123 def copy(self) -> 'FSMData':
124 """Create a shallow copy."""
125 return FSMData(self._data.copy())
127 def deepcopy(self) -> 'FSMData':
128 """Create a deep copy."""
129 return FSMData(copy.deepcopy(self._data))
131 def pop(self, key: str, default: Any = None) -> Any:
132 """Remove and return value."""
133 return self._data.pop(key, default)
135 def setdefault(self, key: str, default: Any = None) -> Any:
136 """Set default value if key doesn't exist."""
137 return self._data.setdefault(key, default)
139 # Conversion methods
140 def to_dict(self) -> Dict[str, Any]:
141 """Convert to plain dictionary.
143 Returns:
144 The underlying data dictionary.
145 """
146 return self._data
148 def __json__(self) -> Dict[str, Any]:
149 """Support JSON serialization via json.dumps with default handler.
151 Returns:
152 The underlying data dictionary for JSON serialization.
153 """
154 return self._data
156 @classmethod
157 def from_dict(cls, data: Dict[str, Any]) -> 'FSMData':
158 """Create from dictionary.
160 Args:
161 data: Dictionary to wrap.
163 Returns:
164 New FSMData instance.
165 """
166 return cls(data)
168 # Special methods for compatibility
169 def __repr__(self) -> str:
170 """String representation."""
171 return f"FSMData({self._data!r})"
173 def __str__(self) -> str:
174 """String conversion."""
175 return str(self._data)
177 def __eq__(self, other: object) -> bool:
178 """Equality comparison."""
179 if isinstance(other, FSMData):
180 return self._data == other._data
181 elif isinstance(other, dict):
182 return self._data == other
183 return False
185 def __bool__(self) -> bool:
186 """Boolean conversion."""
187 return bool(self._data)
190class StateDataWrapper:
191 """Wrapper for state data that provides backward compatibility.
193 This wrapper is used for inline lambda functions that expect
194 `state.data` access pattern. It wraps the FSMData to provide
195 the expected interface.
196 """
198 data: Dict[str, Any] # Always stores the raw dict
199 _fsm_data: FSMData # The FSMData wrapper
201 def __init__(self, data: Union[Dict[str, Any], FSMData, Any] = None):
202 """Initialize state wrapper.
204 Args:
205 data: Data to wrap (dict or FSMData).
206 """
207 # Always expose the underlying dict for lambdas
208 if isinstance(data, FSMData):
209 self.data = data._data # Expose raw dict
210 self._fsm_data = data
211 elif isinstance(data, dict):
212 self.data = data # Expose raw dict
213 self._fsm_data = FSMData(data)
214 else:
215 # Convert to dict
216 data_dict = dict(data) if data else {}
217 self.data = data_dict
218 self._fsm_data = FSMData(data_dict)
220 def __getattr__(self, name: str) -> Any:
221 """Forward attribute access to data."""
222 return getattr(self.data, name)
224 def __getitem__(self, key: str) -> Any:
225 """Forward dict-style access to data."""
226 return self.data[key]
228 def __setitem__(self, key: str, value: Any) -> None:
229 """Forward dict-style setting to data."""
230 self.data[key] = value
233def ensure_dict(data: Union[Dict[str, Any], FSMData, StateDataWrapper, Any]) -> Dict[str, Any]:
234 """Ensure data is a plain dictionary.
236 This utility function converts various data types to a plain dict,
237 which is what user functions expect to receive.
239 Args:
240 data: Data in any supported format.
242 Returns:
243 Plain dictionary.
244 """
245 if isinstance(data, dict):
246 return data
247 elif isinstance(data, FSMData):
248 return data.to_dict()
249 elif isinstance(data, StateDataWrapper):
250 return data.data.to_dict()
251 elif hasattr(data, '_data'):
252 # Handle other wrapper types
253 return data._data
254 elif hasattr(data, 'data'):
255 # Handle objects with data attribute
256 inner = data.data
257 if isinstance(inner, dict):
258 return inner
259 elif isinstance(inner, FSMData):
260 return inner.to_dict()
261 elif hasattr(inner, '_data'):
262 return inner._data
263 # Last resort - try to convert
264 return dict(data) if data else {}
267def wrap_for_lambda(data: Union[Dict[str, Any], FSMData]) -> StateDataWrapper:
268 """Wrap data for inline lambda functions.
270 This creates a wrapper that provides the `state.data` access pattern
271 expected by inline lambda functions in the FSM configuration.
273 Args:
274 data: Data to wrap.
276 Returns:
277 StateDataWrapper instance.
278 """
279 return StateDataWrapper(data)