-
-
Notifications
You must be signed in to change notification settings - Fork 109
Expand file tree
/
Copy pathconfiguration.py
More file actions
169 lines (143 loc) · 5.85 KB
/
Copy pathconfiguration.py
File metadata and controls
169 lines (143 loc) · 5.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
from collections.abc import Mapping
from collections.abc import MutableSet
from typing import TYPE_CHECKING
from typing import Any
from .exceptions import InvalidStateValue
from .i18n import _
from .orderedset import OrderedSet
_SENTINEL = object()
if TYPE_CHECKING:
from .state import State
class Configuration:
"""Encapsulates the dual representation of the active state configuration.
Internally, ``current_state_value`` is either a scalar (single active state)
or an ``OrderedSet`` (parallel regions). This class hides that detail behind
a uniform interface for reading, mutating, and caching the resolved
``OrderedSet[State]``.
"""
__slots__ = (
"_instance_states",
"_model",
"_state_field",
"_states_map",
"_cached",
"_cached_value",
)
def __init__(
self,
instance_states: "Mapping[Any, State]",
model: Any,
state_field: str,
states_map: "dict[Any, State]",
):
self._instance_states = instance_states
self._model = model
self._state_field = state_field
self._states_map = states_map
self._cached: "OrderedSet[State] | None" = None
self._cached_value: Any = _SENTINEL
# -- Raw value (persisted on the model) ------------------------------------
@property
def value(self) -> Any:
"""The raw state value stored on the model (scalar or ``OrderedSet``)."""
return getattr(self._model, self._state_field, None)
@value.setter
def value(self, val: Any):
if val is None:
self._write_to_model(OrderedSet())
elif isinstance(val, MutableSet):
self._write_to_model(OrderedSet(val) if not isinstance(val, OrderedSet) else val)
else:
self._write_to_model(OrderedSet([val]))
@property
def values(self) -> OrderedSet[Any]:
"""The set of raw state values currently active."""
return self._read_from_model()
# -- Resolved states -------------------------------------------------------
@property
def states(self) -> "OrderedSet[State]":
"""The set of currently active :class:`State` instances (cached)."""
raw = self.value
# Snapshot the cache fields locally — another thread may call
# ``_invalidate()`` between the freshness check and the return,
# so reading ``self._cached`` twice would risk returning ``None``.
cached = self._cached
cached_value = self._cached_value
if cached is not None and cached_value is raw:
return cached
if raw is None:
return OrderedSet()
# Normalize inline (avoid second getattr via _read_from_model)
values = raw if isinstance(raw, MutableSet) else (raw,)
result = OrderedSet(self._instance_states[v] for v in values)
self._cached = result
self._cached_value = raw
return result
@states.setter
def states(self, new_configuration: "OrderedSet[State]"):
self._write_to_model(OrderedSet(s.value for s in new_configuration))
# -- Incremental mutation (used by the engine) -----------------------------
def add(self, state: "State"):
"""Add *state* to the configuration (copy-on-write for thread safety)."""
# Copy so we never mutate the OrderedSet still held by concurrent
# readers or by the cache identity check. ``_read_from_model`` may
# return the same ref stored on the model.
values = OrderedSet(self._read_from_model())
values.add(state.value)
self._write_to_model(values)
def discard(self, state: "State"):
"""Remove *state* from the configuration (copy-on-write for thread safety)."""
values = OrderedSet(self._read_from_model())
values.discard(state.value)
self._write_to_model(values)
# -- Deprecated v2 compat --------------------------------------------------
@property
def current_state(self) -> "State | OrderedSet[State]":
"""Resolve the current state with validation.
Unlike ``states`` (which returns an empty set for ``None``), this
raises ``InvalidStateValue`` when the value is ``None`` or not
found in ``states_map`` — matching the v2 ``current_state`` contract.
"""
csv = self.value
if csv is None:
raise InvalidStateValue(
csv,
_(
"There's no current state set. In async code, "
"did you activate the initial state? "
"(e.g., `await sm.activate_initial_state()`)"
),
)
try:
config = self.states
if len(config) == 1:
return next(iter(config))
return config
except KeyError as err:
raise InvalidStateValue(csv) from err
# -- Internal: model boundary ----------------------------------------------
def _read_from_model(self) -> OrderedSet:
"""Normalize: model value → always ``OrderedSet``."""
raw = self.value
if raw is None:
return OrderedSet()
if isinstance(raw, OrderedSet):
return raw
if isinstance(raw, MutableSet):
return OrderedSet(raw)
return OrderedSet([raw])
def _write_to_model(self, values: OrderedSet):
"""Denormalize: ``OrderedSet`` → ``None | scalar | OrderedSet`` for model."""
self._invalidate()
if len(values) == 0:
raw = None
elif len(values) == 1:
raw = next(iter(values))
else:
raw = values
if raw is not None and not isinstance(raw, MutableSet) and raw not in self._states_map:
raise InvalidStateValue(raw)
setattr(self._model, self._state_field, raw)
def _invalidate(self):
self._cached = None
self._cached_value = _SENTINEL