Skip to content

Commit b6b5c06

Browse files
committed
feat: add done_state_ naming convention and complete StateChart docs
Add done_state_ prefix handling in factory.py alongside the existing error_ convention. Attributes starting with done_state_ auto-register both the underscore and dot forms (e.g., done_state_quest matches both "done_state_quest" and "done.state.quest"). Only the prefix is replaced, preserving multi-word state names (done_state_lonely_mountain → done.state.lonely_mountain). Simplify existing tests, examples, and docs to use the convention instead of explicit id= parameters. Add comprehensive StateChart documentation: compound states, parallel states, history pseudo-states, eventless transitions, DoneData, delayed events, In() conditions, error.execution, and the new done_state_ convention.
1 parent a0bd732 commit b6b5c06

14 files changed

Lines changed: 1260 additions & 25 deletions

docs/api.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# API
22

3+
## StateChart
4+
5+
```{versionadded} 3.0.0
6+
```
7+
8+
```{eval-rst}
9+
.. autoclass:: statemachine.statemachine.StateChart
10+
:members:
11+
:undoc-members:
12+
```
13+
314
## StateMachine
415

516
```{eval-rst}
@@ -20,6 +31,16 @@
2031
:members:
2132
```
2233

34+
## HistoryState
35+
36+
```{versionadded} 3.0.0
37+
```
38+
39+
```{eval-rst}
40+
.. autoclass:: statemachine.state.HistoryState
41+
:members:
42+
```
43+
2344
## States (class)
2445

2546
```{eval-rst}
@@ -79,3 +100,12 @@
79100
.. autoclass:: statemachine.event_data.EventData
80101
:members:
81102
```
103+
104+
## create_machine_class_from_definition
105+
106+
```{versionadded} 3.0.0
107+
```
108+
109+
```{eval-rst}
110+
.. autofunction:: statemachine.io.create_machine_class_from_definition
111+
```

docs/async.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,20 @@ before the event is handled:
184184
Initial
185185

186186
```
187+
188+
## StateChart async support
189+
190+
```{versionadded} 3.0.0
191+
```
192+
193+
`StateChart` works identically with the async engine. All statechart features —
194+
compound states, parallel states, history pseudo-states, eventless transitions,
195+
and `done.state` events — are fully supported in async code. The same
196+
`activate_initial_state()` pattern applies:
197+
198+
```python
199+
async def run():
200+
sm = MyStateChart()
201+
await sm.activate_initial_state()
202+
await sm.send("event")
203+
```

docs/releases/3.0.0.md

Lines changed: 177 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# StateMachine 3.05.0
1+
# StateMachine 3.0.0
22

33
*Not released yet*
44

@@ -20,10 +20,160 @@ To verify the standard adoption, now the automated tests suite includes several
2020
While these are exiting news for the library and our community, it also introduces several backwards incompatible changes. Due to the major version release, the new behaviour is assumed by default, but we put
2121
a lot of effort to minimize the changes needed in your codebase, and also introduced a few configuration options that you can enable to restore the old behaviour when possible. The following sections navigate to the new features and includes a migration guide.
2222

23+
### Compound states
24+
25+
**Compound states** have inner child states. Use `State.Compound` to define them
26+
with Python class syntax — the class body becomes the state's children:
27+
28+
```py
29+
>>> from statemachine import State, StateChart
30+
31+
>>> class ShireToRoad(StateChart):
32+
... class shire(State.Compound):
33+
... bag_end = State(initial=True)
34+
... green_dragon = State()
35+
... visit_pub = bag_end.to(green_dragon)
36+
...
37+
... road = State(final=True)
38+
... depart = shire.to(road)
39+
40+
>>> sm = ShireToRoad()
41+
>>> set(sm.configuration_values) == {"shire", "bag_end"}
42+
True
43+
44+
>>> sm.send("visit_pub")
45+
>>> "green_dragon" in sm.configuration_values
46+
True
47+
48+
>>> sm.send("depart")
49+
>>> set(sm.configuration_values) == {"road"}
50+
True
51+
52+
```
53+
54+
Entering a compound activates both the parent and its initial child. Exiting removes
55+
the parent and all descendants. See {ref}`statecharts` for full details.
56+
57+
### Parallel states
58+
59+
**Parallel states** activate all child regions simultaneously. Use `State.Parallel`:
60+
61+
```py
62+
>>> from statemachine import State, StateChart
63+
64+
>>> class WarOfTheRing(StateChart):
65+
... validate_disconnected_states = False
66+
... class war(State.Parallel):
67+
... class frodos_quest(State.Compound):
68+
... shire = State(initial=True)
69+
... mordor = State(final=True)
70+
... journey = shire.to(mordor)
71+
... class aragorns_path(State.Compound):
72+
... ranger = State(initial=True)
73+
... king = State(final=True)
74+
... coronation = ranger.to(king)
75+
76+
>>> sm = WarOfTheRing()
77+
>>> "shire" in sm.configuration_values and "ranger" in sm.configuration_values
78+
True
79+
80+
>>> sm.send("journey")
81+
>>> "mordor" in sm.configuration_values and "ranger" in sm.configuration_values
82+
True
83+
84+
```
85+
86+
Events in one region don't affect others. See {ref}`statecharts` for full details.
87+
2388
### History pseudo-states
2489

25-
The **History pseudo-state** is a special state that is used to record the configuration of the state machine when leaving a compound state. When the state machine transitions into a history state, it will automatically transition to the state that was previously recorded. This allows the state machine to remember the configuration of its child states.
90+
The **History pseudo-state** records the configuration of a compound state when it
91+
is exited. Re-entering via the history state restores the previously active child.
92+
Supports both shallow (`HistoryState()`) and deep (`HistoryState(deep=True)`) history:
2693

94+
```py
95+
>>> from statemachine import HistoryState, State, StateChart
96+
97+
>>> class GollumPersonality(StateChart):
98+
... validate_disconnected_states = False
99+
... class personality(State.Compound):
100+
... smeagol = State(initial=True)
101+
... gollum = State()
102+
... h = HistoryState()
103+
... dark_side = smeagol.to(gollum)
104+
... light_side = gollum.to(smeagol)
105+
... outside = State()
106+
... leave = personality.to(outside)
107+
... return_via_history = outside.to(personality.h)
108+
109+
>>> sm = GollumPersonality()
110+
>>> sm.send("dark_side")
111+
>>> "gollum" in sm.configuration_values
112+
True
113+
114+
>>> sm.send("leave")
115+
>>> sm.send("return_via_history")
116+
>>> "gollum" in sm.configuration_values
117+
True
118+
119+
```
120+
121+
See {ref}`statecharts` for full details on shallow vs deep history.
122+
123+
### Eventless (automatic) transitions
124+
125+
Transitions without an event trigger fire automatically when their guard condition
126+
is met:
127+
128+
```py
129+
>>> from statemachine import State, StateChart
130+
131+
>>> class BeaconChain(StateChart):
132+
... class beacons(State.Compound):
133+
... first = State(initial=True)
134+
... second = State()
135+
... last = State(final=True)
136+
... first.to(second)
137+
... second.to(last)
138+
... signal_received = State(final=True)
139+
... done_state_beacons = beacons.to(signal_received)
140+
141+
>>> sm = BeaconChain()
142+
>>> set(sm.configuration_values) == {"signal_received"}
143+
True
144+
145+
```
146+
147+
The entire eventless chain cascades in a single macrostep. See {ref}`statecharts`.
148+
149+
### DoneData on final states
150+
151+
Final states can provide data to `done.state` handlers via the `donedata` parameter:
152+
153+
```py
154+
>>> from statemachine import Event, State, StateChart
155+
156+
>>> class QuestCompletion(StateChart):
157+
... class quest(State.Compound):
158+
... traveling = State(initial=True)
159+
... completed = State(final=True, donedata="get_result")
160+
... finish = traveling.to(completed)
161+
... def get_result(self):
162+
... return {"hero": "frodo", "outcome": "victory"}
163+
... epilogue = State(final=True)
164+
... done_state_quest = Event(quest.to(epilogue, on="capture_result"))
165+
... def capture_result(self, hero=None, outcome=None, **kwargs):
166+
... self.result = f"{hero}: {outcome}"
167+
168+
>>> sm = QuestCompletion()
169+
>>> sm.send("finish")
170+
>>> sm.result
171+
'frodo: victory'
172+
173+
```
174+
175+
The `done_state_` naming convention automatically registers the `done.state.{suffix}`
176+
form — no explicit `id=` needed. See {ref}`done-state-convention` for details.
27177

28178
### Create state machine class from a dict definition
29179

@@ -128,12 +278,22 @@ for full details.
128278

129279
### Delayed events
130280

131-
Specify an event to run in the near future. The engine will keep track of the execution time
132-
and only process the event when `now > execution_time`.
281+
Specify an event to run in the near future using `delay` (in milliseconds). The engine
282+
will keep track of the execution time and only process the event when `now > execution_time`.
133283

134-
TODO: Example of delayed events
284+
```python
285+
# Send with delay
286+
sm.send("light_beacons", delay=500) # fires after 500ms
135287

136-
Also, delayed events can be revoked by it's `send_id`.
288+
# Define delay on the Event itself
289+
light = Event(dark.to(lit), delay=100)
290+
291+
# Cancel a delayed event before it fires
292+
sm.send("light_beacons", delay=5000, event_id="beacon_signal")
293+
sm.cancel_event("beacon_signal") # event is removed from the queue
294+
```
295+
296+
Also, delayed events can be revoked by their `send_id` using `sm.cancel_event(send_id)`.
137297

138298

139299
### Disable single graph component validation.
@@ -152,14 +312,24 @@ It's already disabled when parsing SCXML files.
152312

153313
TODO.
154314

315+
## Known limitations
316+
317+
The following SCXML features are **not yet implemented** and are deferred to a future release:
318+
319+
- `<invoke>` — invoking external services or sub-machines from within a state
320+
- HTTP and other external communication targets
321+
- `<finalize>` — processing data returned from invoked services
322+
323+
These features are tracked for v3.1+.
324+
155325
## Backward incompatible changes in 3.0
156326

157327

158328
### Python compatibility in 3.0.0
159329

160330
We've dropped support for Python `3.7` and `3.8`. If you need support for these versios use the 2.* series.
161331

162-
StateMachine 3.0.0 supports Python 3.9, 3.10, 3.11, 3.12, and 3.13.
332+
StateMachine 3.0.0 supports Python 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
163333

164334

165335
### Non-RTC model removed

0 commit comments

Comments
 (0)