Skip to content

Commit 67f2b0d

Browse files
authored
Refactor/configuration normalize orderedset (#599)
* test: add API contract tests for Configuration across all topologies Systematic test matrix covering the observable behavior of all public Configuration APIs (current_state_value, configuration_values, configuration, current_state, model.state) across flat, compound, parallel, and complex parallel StateCharts. Verifies type contracts (scalar vs OrderedSet), value correctness, model identity, and the uninitialized async lifecycle. Uses pytest.parametrize over 14 topology x lifecycle scenarios x sync/async engines, plus setter and uninitialized edge cases (39 tests total). * refactor: normalize Configuration internals to always use OrderedSet Introduce two boundary functions (_read_from_model / _write_to_model) that confine the None|scalar|OrderedSet trichotomy to the model edge. All other methods (add, discard, states setter, values) now operate on a uniform OrderedSet, eliminating per-method type branching. The model still receives the same denormalized values as before (None for empty, scalar for single state, OrderedSet for multiple). Fixes edge case: configuration_values now returns OrderedSet() for uninitialized config instead of OrderedSet([None]), since None is not a valid state value. Prepares the codebase for the persistence protocol work (#597).
1 parent 8d17ba9 commit 67f2b0d

File tree

4 files changed

+365
-47
lines changed

4 files changed

+365
-47
lines changed

docs/async.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ the initial state:
7575
... print(list(sm.configuration_values))
7676

7777
>>> asyncio.run(show_problem())
78-
[None]
78+
[]
7979

8080
```
8181

statemachine/configuration.py

Lines changed: 48 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -55,75 +55,53 @@ def value(self) -> Any:
5555

5656
@value.setter
5757
def value(self, val: Any):
58-
self._invalidate()
59-
if val is not None and not isinstance(val, MutableSet) and val not in self._states_map:
60-
raise InvalidStateValue(val)
61-
setattr(self._model, self._state_field, val)
58+
if val is None:
59+
self._write_to_model(OrderedSet())
60+
elif isinstance(val, MutableSet):
61+
self._write_to_model(OrderedSet(val) if not isinstance(val, OrderedSet) else val)
62+
else:
63+
self._write_to_model(OrderedSet([val]))
6264

6365
@property
6466
def values(self) -> OrderedSet[Any]:
6567
"""The set of raw state values currently active."""
66-
v = self.value
67-
if isinstance(v, OrderedSet):
68-
return v
69-
return OrderedSet([v])
68+
return self._read_from_model()
7069

7170
# -- Resolved states -------------------------------------------------------
7271

7372
@property
7473
def states(self) -> "OrderedSet[State]":
7574
"""The set of currently active :class:`State` instances (cached)."""
76-
csv = self.value
77-
if self._cached is not None and self._cached_value is csv:
75+
raw = self.value
76+
if self._cached is not None and self._cached_value is raw:
7877
return self._cached
79-
if csv is None:
78+
if raw is None:
8079
return OrderedSet()
8180

82-
instance_states = self._instance_states
83-
if not isinstance(csv, MutableSet):
84-
result = OrderedSet([instance_states[self._states_map[csv].id]])
85-
else:
86-
result = OrderedSet([instance_states[self._states_map[v].id] for v in csv])
87-
81+
# Normalize inline (avoid second getattr via _read_from_model)
82+
values = raw if isinstance(raw, MutableSet) else (raw,)
83+
result = OrderedSet(self._instance_states[self._states_map[v].id] for v in values)
8884
self._cached = result
89-
self._cached_value = csv
85+
self._cached_value = raw
9086
return result
9187

9288
@states.setter
9389
def states(self, new_configuration: "OrderedSet[State]"):
94-
if len(new_configuration) == 0:
95-
self.value = None
96-
elif len(new_configuration) == 1:
97-
self.value = next(iter(new_configuration)).value
98-
else:
99-
self.value = OrderedSet(s.value for s in new_configuration)
90+
self._write_to_model(OrderedSet(s.value for s in new_configuration))
10091

10192
# -- Incremental mutation (used by the engine) -----------------------------
10293

10394
def add(self, state: "State"):
104-
"""Add *state* to the configuration, maintaining the dual representation."""
105-
csv = self.value
106-
if csv is None:
107-
self.value = state.value
108-
elif isinstance(csv, MutableSet):
109-
csv.add(state.value)
110-
self.value = csv
111-
else:
112-
self.value = OrderedSet([csv, state.value])
95+
"""Add *state* to the configuration."""
96+
values = self._read_from_model()
97+
values.add(state.value)
98+
self._write_to_model(values)
11399

114100
def discard(self, state: "State"):
115-
"""Remove *state* from the configuration, normalizing back to scalar."""
116-
csv = self.value
117-
if isinstance(csv, MutableSet):
118-
csv.discard(state.value)
119-
if len(csv) == 0:
120-
self.value = None
121-
elif len(csv) == 1:
122-
self.value = next(iter(csv))
123-
else:
124-
self.value = csv
125-
elif csv == state.value:
126-
self.value = None
101+
"""Remove *state* from the configuration."""
102+
values = self._read_from_model()
103+
values.discard(state.value)
104+
self._write_to_model(values)
127105

128106
# -- Deprecated v2 compat --------------------------------------------------
129107

@@ -153,7 +131,31 @@ def current_state(self) -> "State | OrderedSet[State]":
153131
except KeyError as err:
154132
raise InvalidStateValue(csv) from err
155133

156-
# -- Internal --------------------------------------------------------------
134+
# -- Internal: model boundary ----------------------------------------------
135+
136+
def _read_from_model(self) -> OrderedSet:
137+
"""Normalize: model value → always ``OrderedSet``."""
138+
raw = self.value
139+
if raw is None:
140+
return OrderedSet()
141+
if isinstance(raw, OrderedSet):
142+
return raw
143+
if isinstance(raw, MutableSet):
144+
return OrderedSet(raw)
145+
return OrderedSet([raw])
146+
147+
def _write_to_model(self, values: OrderedSet):
148+
"""Denormalize: ``OrderedSet`` → ``None | scalar | OrderedSet`` for model."""
149+
self._invalidate()
150+
if len(values) == 0:
151+
raw = None
152+
elif len(values) == 1:
153+
raw = next(iter(values))
154+
else:
155+
raw = values
156+
if raw is not None and not isinstance(raw, MutableSet) and raw not in self._states_map:
157+
raise InvalidStateValue(raw)
158+
setattr(self._model, self._state_field, raw)
157159

158160
def _invalidate(self):
159161
self._cached = None

0 commit comments

Comments
 (0)