Skip to content

Commit a3d30a1

Browse files
fgmacedoclaude
andcommitted
docs: comprehensive v3 documentation rewrite
Rewrite core documentation pages for the v3 release with SCXML-compliant statechart semantics, consistent narrative, and testable doctests throughout. New pages: - concepts.md: hybrid narrative + reference overview of states, transitions, events, and actions - quickstart.md: progressive tutorial from flat FSM to full statechart using a coffee shop domain - events.md: extracted from transitions.md; declaring, triggering, send vs raise_(), delayed, done.state, error events - error_handling.md: error.execution lifecycle, block-level catching, cleanup - validations.md: consolidated validation checks (reachability, trap states, final state constraints) Rewritten pages: - actions.md: execution order table with all 9 groups, SCXML exit/enter semantics for compounds, full invoke documentation, DI with previous/new_configuration, priority within groups - states.md: pedagogical intro, parameters table, removed autoclass dump and deprecated current_state references - transitions.md: extracted events, added from_(), from_.any(), multiple targets, cross-boundary, transition priority - index.md: reorganized toctree into Getting Started, Core Concepts, Engine, Advanced, Reference sections Minor updates: processing_model.md labels, models.md, guards.md, listeners.md, async.md, diagram.md, integrations.md, statecharts.md, conf.py seealso links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 300cb34 commit a3d30a1

21 files changed

Lines changed: 2763 additions & 796 deletions

docs/actions.md

Lines changed: 677 additions & 329 deletions
Large diffs are not rendered by default.

docs/async.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
(async)=
12
# Async
23

34
```{versionadded} 2.3.0
@@ -68,10 +69,12 @@ Reuses external loop
6869
All handlers will run on the same thread they are called. Therefore, mixing synchronous and asynchronous code is not recommended unless you are confident in your implementation.
6970
```
7071

72+
(syncengine)=
7173
### SyncEngine
7274
Activated if there are no async callbacks. All code runs exactly as it did before version 2.3.0.
7375
There's no event loop.
7476

77+
(asyncengine)=
7578
### AsyncEngine
7679
Activated if there is at least one async callback. The code runs asynchronously and requires a running event loop, which it will create if none exists.
7780

docs/concepts.md

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
(concepts)=
2+
3+
# Core concepts
4+
5+
A statechart organizes behavior around **states**, **transitions**, and
6+
**events**. Together they describe *when* the system can change, *what*
7+
triggers the change, and *what happens* as a result.
8+
9+
```py
10+
>>> from statemachine import StateChart, State
11+
12+
>>> class Turnstile(StateChart):
13+
... locked = State(initial=True)
14+
... unlocked = State()
15+
...
16+
... coin = locked.to(unlocked, on="thank_you")
17+
... push = unlocked.to(locked)
18+
...
19+
... def thank_you(self):
20+
... return "Welcome!"
21+
22+
>>> sm = Turnstile()
23+
>>> sm.coin()
24+
'Welcome!'
25+
26+
```
27+
28+
Even in this minimal example, all five core concepts appear:
29+
30+
31+
(concepts-states)=
32+
## States
33+
34+
A **state** describes what the system is doing right now. At any point in
35+
time, a statechart is "in" one or more states — the **configuration**. States
36+
determine which transitions are available and which events are accepted.
37+
38+
In the turnstile example, `locked` and `unlocked` are the two possible
39+
states. The machine starts in `locked` (its **initial state**) and can only
40+
reach `unlocked` when the `coin` event fires.
41+
42+
```{seealso}
43+
See [](states.md) for the full reference: initial and final states, compound
44+
(nested) states, parallel regions, history pseudo-states, and more.
45+
```
46+
47+
48+
(concepts-transitions)=
49+
## Transitions
50+
51+
A **transition** is a link between a **source** state and a **target** state.
52+
When a transition fires, the system leaves the source and enters the target.
53+
Transitions can carry {ref}`actions` (side-effects) and
54+
{ref}`conditions <validators and guards>` (guards that must be satisfied).
55+
56+
In the turnstile, `locked.to(unlocked)` is a transition: it moves the system
57+
from `locked` to `unlocked` and runs the `thank_you` action along the way.
58+
59+
```{seealso}
60+
See [](transitions.md) for the full reference: declaring transitions,
61+
self-transitions, internal transitions, eventless (automatic) transitions,
62+
and more.
63+
```
64+
65+
66+
(concepts-events)=
67+
## Events
68+
69+
An **event** is a signal that something has happened. Events trigger
70+
transitions — without an event, a transition will not fire (unless it is
71+
an {ref}`eventless <eventless>` transition with a guard condition).
72+
73+
In the turnstile, `coin` and `push` are events. When you call `sm.coin()` or
74+
`sm.send("coin")`, the engine looks for a matching transition from the current
75+
state and fires it. Events are processed following a **run-to-completion**
76+
model — each event is fully handled before the next one starts.
77+
78+
```{seealso}
79+
See [](events.md) for the full reference: declaring, triggering, scheduling,
80+
and naming conventions. See [](processing_model.md) for how macrosteps and
81+
microsteps work under the hood.
82+
```
83+
84+
85+
(concepts-actions)=
86+
## Actions
87+
88+
An **action** is a side-effect that runs during a transition or on
89+
entry/exit of a state. Actions are how the statechart interacts with the
90+
outside world — sending notifications, updating a database, logging,
91+
or returning a value.
92+
93+
In the turnstile, `thank_you` is an action attached to the `coin` transition
94+
via the `on` parameter.
95+
96+
```{seealso}
97+
See [](actions.md) for the full reference: callback naming conventions,
98+
execution order, dependency injection, and all available hooks.
99+
```
100+
101+
102+
(concepts-conditions)=
103+
## Conditions and validators
104+
105+
A **condition** (also called a **guard**) is a predicate that must evaluate
106+
to `True` for a transition to fire. A **validator** is similar but raises an
107+
exception to block the transition instead of silently preventing it.
108+
109+
Conditions let you have multiple transitions for the same event, each with a
110+
different guard — the first one that passes wins.
111+
112+
```{seealso}
113+
See [](guards.md) for the full reference: `cond`, `unless`, `validators`,
114+
boolean expressions, and the `In()` condition.
115+
```
116+
117+
118+
## Quick reference
119+
120+
| Concept | What it is | Declared as |
121+
|---------------|-----------------------------------------|-----------------------------------|
122+
| **State** | A mode or condition of the system | `State()`, `State.Compound`, `State.Parallel` |
123+
| **Transition**| A link from source state to target state| `source.to(target)`, `target.from_(source)` |
124+
| **Event** | A signal that triggers transitions | Class-level assignment or `Event(...)` |
125+
| **Action** | A side-effect during state changes | Callbacks: `on`, `before`, `after`, `enter`, `exit` |
126+
| **Condition** | A guard that allows/blocks a transition | `cond`, `unless`, `validators` parameters |

docs/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@
5353
"sphinx_copybutton",
5454
]
5555

56+
autosectionlabel_prefix_document = True
57+
5658
# Add any paths that contain templates here, relative to this directory.
5759
templates_path = ["_templates"]
5860

docs/diagram.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
(diagram)=
2+
(diagrams)=
13
# Diagrams
24

35
You can generate diagrams from your {ref}`StateChart`.

docs/error_handling.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
2+
(error-handling)=
3+
# Error handling
4+
5+
When callbacks raise exceptions during a transition, the state machine needs
6+
a strategy. This library supports two models: **exception propagation** (the
7+
traditional approach) and **error events** (the SCXML-compliant approach).
8+
9+
## Two models
10+
11+
| Approach | `error_on_execution` | Behavior |
12+
|-----------------------|----------------------|-------------------------------------------------------|
13+
| Exception propagation | `False` | Exceptions bubble up to the caller like normal Python. |
14+
| Error events | `True` | Exceptions are caught and dispatched as `error.execution` internal events. |
15+
16+
`StateChart` uses `error_on_execution = True` by default — the SCXML-compliant
17+
behavior. You can switch to exception propagation by overriding the attribute:
18+
19+
```python
20+
from statemachine import StateChart
21+
22+
class MyChart(StateChart):
23+
error_on_execution = False # exceptions propagate to the caller
24+
```
25+
26+
### When to use which
27+
28+
**Exception propagation** (`error_on_execution = False`) is simpler and
29+
familiar to most Python developers. It works well for flat state machines
30+
where errors should stop execution immediately and be handled by the caller
31+
with `try/except`.
32+
33+
**Error events** (`error_on_execution = True`) are the SCXML standard and
34+
the recommended approach for statecharts. They allow the machine to handle
35+
errors as part of its own logic — transitioning to error states, retrying,
36+
or recovering — without leaking implementation details to the caller. This
37+
is especially powerful in hierarchical machines where different states may
38+
handle errors differently.
39+
40+
41+
(error-execution)=
42+
## Error events with `error.execution`
43+
44+
When `error_on_execution` is `True`, runtime exceptions during transitions
45+
are caught by the engine and result in an internal `error.execution` event
46+
being placed on the queue. This follows the
47+
[SCXML error handling specification](https://www.w3.org/TR/scxml/#errorsAndEvents).
48+
49+
You can define transitions for this event to gracefully handle errors within
50+
the state machine itself.
51+
52+
### The `error_` naming convention
53+
54+
Since Python identifiers cannot contain dots, the library provides a naming
55+
convention: any event attribute starting with `error_` automatically matches
56+
both the underscore form and the dot-notation form. For example,
57+
`error_execution` matches both `"error_execution"` and `"error.execution"`.
58+
59+
```py
60+
>>> from statemachine import State, StateChart
61+
62+
>>> class MyChart(StateChart):
63+
... s1 = State("s1", initial=True)
64+
... error_state = State("error_state", final=True)
65+
...
66+
... go = s1.to(s1, on="bad_action")
67+
... error_execution = s1.to(error_state)
68+
...
69+
... def bad_action(self):
70+
... raise RuntimeError("something went wrong")
71+
72+
>>> sm = MyChart()
73+
>>> sm.send("go")
74+
>>> sm.configuration == {sm.error_state}
75+
True
76+
77+
```
78+
79+
This is equivalent to the more verbose explicit form:
80+
81+
```python
82+
error_execution = Event(s1.to(error_state), id="error.execution")
83+
```
84+
85+
The convention works with both bare transitions and `Event` objects without
86+
an explicit `id`:
87+
88+
```py
89+
>>> from statemachine import Event, State, StateChart
90+
91+
>>> class ChartWithEvent(StateChart):
92+
... s1 = State("s1", initial=True)
93+
... error_state = State("error_state", final=True)
94+
...
95+
... go = s1.to(s1, on="bad_action")
96+
... error_execution = Event(s1.to(error_state))
97+
...
98+
... def bad_action(self):
99+
... raise RuntimeError("something went wrong")
100+
101+
>>> sm = ChartWithEvent()
102+
>>> sm.send("go")
103+
>>> sm.configuration == {sm.error_state}
104+
True
105+
106+
```
107+
108+
```{note}
109+
If you provide an explicit `id=` parameter, it takes precedence and the naming
110+
convention is not applied.
111+
```
112+
113+
### Accessing error data
114+
115+
The error object is passed as `error` in the keyword arguments to callbacks
116+
on the `error.execution` transition:
117+
118+
```py
119+
>>> from statemachine import State, StateChart
120+
121+
>>> class ErrorDataChart(StateChart):
122+
... s1 = State("s1", initial=True)
123+
... error_state = State("error_state", final=True)
124+
...
125+
... go = s1.to(s1, on="bad_action")
126+
... error_execution = s1.to(error_state, on="handle_error")
127+
...
128+
... def bad_action(self):
129+
... raise RuntimeError("specific error")
130+
...
131+
... def handle_error(self, error=None, **kwargs):
132+
... self.last_error = error
133+
134+
>>> sm = ErrorDataChart()
135+
>>> sm.send("go")
136+
>>> str(sm.last_error)
137+
'specific error'
138+
139+
```
140+
141+
142+
### Error-in-error-handler behavior
143+
144+
If an error occurs while processing the `error.execution` event itself, the
145+
engine ignores the second error (logging a warning) to prevent infinite loops.
146+
The state machine remains in the configuration it was in before the failed
147+
error handler.
148+
149+
150+
## Block-level error catching
151+
152+
`StateChart` catches errors at the **block level**, not the microstep level.
153+
Each phase of the microstep — `on_exit`, transition `on` content, `on_enter`
154+
— is an independent block. An error in one block:
155+
156+
- **Stops remaining actions in that block** (per SCXML spec, execution MUST
157+
NOT continue within the same block after an error).
158+
- **Does not affect other blocks** — subsequent phases of the microstep still
159+
execute. In particular, `after` callbacks always run regardless of errors
160+
in earlier blocks.
161+
162+
This means that even if a transition's `on` action raises an exception, the
163+
transition completes: target states are entered and `after_<event>()` callbacks
164+
still run. The error is caught and queued as an `error.execution` internal
165+
event, which can be handled by a separate transition.
166+
167+
```{note}
168+
During `error.execution` processing, errors in transition `on` content are
169+
**not** caught at block level — they propagate to the microstep, where they
170+
are silently ignored. This prevents infinite loops when an error handler's own
171+
action raises (e.g., a self-transition `error_execution = s1.to(s1, on="handler")`
172+
where `handler` raises). Entry/exit blocks always use block-level error
173+
catching regardless of the current event.
174+
```
175+
176+
177+
(error-handling-cleanup-finalize)=
178+
## Cleanup / finalize pattern
179+
180+
A common need is to run cleanup code after a transition **regardless of
181+
success or failure** — for example, releasing a lock or closing a resource.
182+
183+
Because `StateChart` catches errors at the block level (see above),
184+
`after_<event>()` callbacks still run even when an action raises an exception.
185+
This makes `after_<event>()` a natural **finalize** hook — no need to
186+
duplicate cleanup logic in an error handler.
187+
188+
For error-specific handling (logging, recovery), define an `error.execution`
189+
transition and use {func}`raise_() <StateChart.raise_>` to auto-recover
190+
within the same macrostep.
191+
192+
See the full working example in {ref}`sphx_glr_auto_examples_statechart_cleanup_machine.py`.
193+
194+
195+
```{seealso}
196+
See {ref}`statecharts` for the full comparison of `StateChart` behavioral
197+
attributes and how to customize them.
198+
```

0 commit comments

Comments
 (0)