Skip to content

Commit 4d029e7

Browse files
committed
docs: add v3 upgrade guide and migrate documentation to StateChart API
Add comprehensive upgrade guide from 2.x to 3.0 covering all breaking changes with before/after examples. Update all documentation to use StateChart as the primary API with non-deprecated methods/properties, replacing StateMachine, current_state, and add_observer references.
1 parent a07c8a4 commit 4d029e7

16 files changed

Lines changed: 483 additions & 101 deletions

conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ def add_doctest_context(doctest_namespace): # noqa: PT004
99
from statemachine.utils import run_async_from_sync
1010

1111
from statemachine import State
12+
from statemachine import StateChart
1213
from statemachine import StateMachine
1314

1415
class ContribAsyncio:
@@ -23,6 +24,7 @@ def __init__(self):
2324
self.run = run_async_from_sync
2425

2526
doctest_namespace["State"] = State
27+
doctest_namespace["StateChart"] = StateChart
2628
doctest_namespace["StateMachine"] = StateMachine
2729
doctest_namespace["asyncio"] = ContribAsyncio()
2830

docs/actions.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Actions
22

3-
Action is the way a {ref}`StateMachine` can cause things to happen in the
3+
Action is the way a {ref}`StateChart` can cause things to happen in the
44
outside world, and indeed they are the main reason why they exist at all.
55

66
The main point of introducing a state machine is for the
@@ -11,7 +11,7 @@ Actions are most commonly performed on entry or exit of a state, although
1111
it is possible to add them before/after a transition.
1212

1313
There are several action callbacks that you can define to interact with a
14-
StateMachine in execution.
14+
StateChart in execution.
1515

1616
There are callbacks that you can specify that are generic and will be called
1717
when something changes, and are not bound to a specific state or event:
@@ -31,9 +31,9 @@ when something changes, and are not bound to a specific state or event:
3131
The following example offers an overview of the "generic" callbacks available:
3232

3333
```py
34-
>>> from statemachine import StateMachine, State
34+
>>> from statemachine import StateChart, State
3535

36-
>>> class ExampleStateMachine(StateMachine):
36+
>>> class ExampleStateMachine(StateChart):
3737
... initial = State(initial=True)
3838
... final = State(final=True)
3939
...
@@ -91,7 +91,7 @@ For each defined {ref}`state`, you can declare `enter` and `exit` callbacks.
9191

9292
### Bind state actions by naming convention
9393

94-
Callbacks by naming convention will be searched on the StateMachine and on the
94+
Callbacks by naming convention will be searched on the StateChart and on the
9595
model, using the patterns:
9696

9797
- `on_enter_<state.id>()`
@@ -100,9 +100,9 @@ model, using the patterns:
100100

101101

102102
```py
103-
>>> from statemachine import StateMachine, State
103+
>>> from statemachine import StateChart, State
104104

105-
>>> class ExampleStateMachine(StateMachine):
105+
>>> class ExampleStateMachine(StateChart):
106106
... initial = State(initial=True)
107107
...
108108
... loop = initial.to.itself()
@@ -120,9 +120,9 @@ model, using the patterns:
120120
Use the `enter` or `exit` params available on the `State` constructor.
121121

122122
```py
123-
>>> from statemachine import StateMachine, State
123+
>>> from statemachine import StateChart, State
124124

125-
>>> class ExampleStateMachine(StateMachine):
125+
>>> class ExampleStateMachine(StateChart):
126126
... initial = State(initial=True, enter="entering_initial", exit="leaving_initial")
127127
...
128128
... loop = initial.to.itself()
@@ -143,9 +143,9 @@ It's also possible to use an event name as action.
143143

144144

145145
```py
146-
>>> from statemachine import StateMachine, State
146+
>>> from statemachine import StateChart, State
147147

148-
>>> class ExampleStateMachine(StateMachine):
148+
>>> class ExampleStateMachine(StateChart):
149149
... initial = State(initial=True)
150150
...
151151
... loop = initial.to.itself()
@@ -179,9 +179,9 @@ using the patterns:
179179

180180

181181
```py
182-
>>> from statemachine import StateMachine, State
182+
>>> from statemachine import StateChart, State
183183

184-
>>> class ExampleStateMachine(StateMachine):
184+
>>> class ExampleStateMachine(StateChart):
185185
... initial = State(initial=True)
186186
...
187187
... loop = initial.to.itself()
@@ -201,9 +201,9 @@ using the patterns:
201201
### Bind transition actions using params
202202

203203
```py
204-
>>> from statemachine import StateMachine, State
204+
>>> from statemachine import StateChart, State
205205

206-
>>> class ExampleStateMachine(StateMachine):
206+
>>> class ExampleStateMachine(StateChart):
207207
... initial = State(initial=True)
208208
...
209209
... loop = initial.to.itself(before="just_before", on="its_happening", after="loop_completed")
@@ -229,9 +229,9 @@ The action will be registered for every {ref}`transition` in the list associated
229229

230230

231231
```py
232-
>>> from statemachine import StateMachine, State
232+
>>> from statemachine import StateChart, State
233233

234-
>>> class ExampleStateMachine(StateMachine):
234+
>>> class ExampleStateMachine(StateChart):
235235
... initial = State(initial=True)
236236
...
237237
... loop = initial.to.itself()
@@ -263,9 +263,9 @@ The action will be registered for every {ref}`transition` in the list associated
263263
You can also declare an event while also adding a callback:
264264

265265
```py
266-
>>> from statemachine import StateMachine, State
266+
>>> from statemachine import StateChart, State
267267

268-
>>> class ExampleStateMachine(StateMachine):
268+
>>> class ExampleStateMachine(StateChart):
269269
... initial = State(initial=True)
270270
...
271271
... @initial.to.itself()
@@ -277,7 +277,7 @@ You can also declare an event while also adding a callback:
277277

278278
Note that with this syntax, the resulting `loop` that is present on the `ExampleStateMachine.loop`
279279
namespace is not a simple method, but an {ref}`event` trigger. So it only executes if the
280-
StateMachine is in the right state.
280+
StateChart is in the right state.
281281

282282
So, you can use the event-oriented approach:
283283

@@ -307,7 +307,7 @@ that will be included in `**kwargs` to all other callbacks.
307307
A not so usefull example:
308308

309309
```py
310-
>>> class ExampleStateMachine(StateMachine):
310+
>>> class ExampleStateMachine(StateChart):
311311
... initial = State(initial=True)
312312
...
313313
... loop = initial.to.itself()
@@ -399,7 +399,7 @@ Note that `None` will be used if the action callback does not return anything, b
399399
defined explicitly. The following provides an example:
400400

401401
```py
402-
>>> class ExampleStateMachine(StateMachine):
402+
>>> class ExampleStateMachine(StateChart):
403403
... initial = State(initial=True)
404404
...
405405
... loop = initial.to.itself()
@@ -421,15 +421,15 @@ defined explicitly. The following provides an example:
421421

422422
```
423423

424-
For {ref}`RTC model`, only the main event will get its value list, while the chained ones simply get
425-
`None` returned. For {ref}`Non-RTC model`, results for every event will always be collected and returned.
424+
Only the main event will get its value list, while the chained ones simply get
425+
`None` returned.
426426

427427

428428
(dynamic-dispatch)=
429429
(dynamic dispatch)=
430430
## Dependency injection
431431

432-
{ref}`statemachine` implements a dependency injection mechanism on all available {ref}`Actions` and
432+
{ref}`StateChart` implements a dependency injection mechanism on all available {ref}`Actions` and
433433
{ref}`Conditions` that automatically inspects and matches the expected callback params with those available by the library in conjunction with any values informed when calling an event using `*args` and `**kwargs`.
434434

435435
The library ensures that your method signatures match the expected arguments.

docs/api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ that will be merged into `**kwargs` for all subsequent callbacks (guards, action
114114
entry/exit handlers) during that event's processing:
115115

116116
```python
117-
class MyMachine(StateMachine):
117+
class MyMachine(StateChart):
118118
initial = State(initial=True)
119119
loop = initial.to.itself()
120120

docs/async.md

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Support for async code was added!
55
```
66

7-
The {ref}`StateMachine` fully supports asynchronous code. You can write async {ref}`actions`, {ref}`guards`, and {ref}`events` triggers, while maintaining the same external API for both synchronous and asynchronous codebases.
7+
The {ref}`StateChart` fully supports asynchronous code. You can write async {ref}`actions`, {ref}`guards`, and {ref}`events` triggers, while maintaining the same external API for both synchronous and asynchronous codebases.
88

99
This is achieved through a new concept called **engine**, an internal strategy pattern abstraction that manages transitions and callbacks.
1010

@@ -89,7 +89,7 @@ async code with a state machine.
8989

9090

9191
```py
92-
>>> class AsyncStateMachine(StateMachine):
92+
>>> class AsyncStateMachine(StateChart):
9393
... initial = State('Initial', initial=True)
9494
... final = State('Final', final=True)
9595
...
@@ -103,11 +103,11 @@ async code with a state machine.
103103
... sm = AsyncStateMachine()
104104
... result = await sm.advance()
105105
... print(f"Result is {result}")
106-
... print(sm.current_state)
106+
... print(list(sm.configuration_values))
107107

108108
>>> asyncio.run(run_sm())
109109
Result is 42
110-
Final
110+
['final']
111111

112112
```
113113

@@ -124,8 +124,8 @@ If needed, the state machine will create a loop using `asyncio.new_event_loop()`
124124
>>> result = sm.advance()
125125
>>> print(f"Result is {result}")
126126
Result is 42
127-
>>> print(sm.current_state)
128-
Final
127+
>>> print(list(sm.configuration_values))
128+
['final']
129129

130130
```
131131

@@ -134,26 +134,24 @@ Final
134134
## Initial State Activation for Async Code
135135

136136

137-
If **on async code** you perform checks against the `current_state`, like a loop `while sm.current_state.is_final:`, then you must manually
138-
await for the [activate initial state](statemachine.StateMachine.activate_initial_state) to be able to check the current state.
137+
If **on async code** you perform checks against the `configuration`, like a loop `while not sm.is_terminated:`, then you must manually
138+
await for the [activate initial state](statemachine.StateChart.activate_initial_state) to be able to check the configuration.
139139

140140
```{hint}
141141
This manual initial state activation on async is because Python don't allow awaiting at class initalization time and the initial state activation may contain async callbacks that must be awaited.
142142
```
143143

144-
If you don't do any check for current state externally, just ignore this as the initial state is activated automatically before the first event trigger is handled.
144+
If you don't do any check for configuration externally, just ignore this as the initial state is activated automatically before the first event trigger is handled.
145145

146-
You get an error checking the current state before the initial state activation:
146+
You get an error checking the configuration before the initial state activation:
147147

148148
```py
149149
>>> async def initialize_sm():
150150
... sm = AsyncStateMachine()
151-
... print(sm.current_state)
151+
... print(list(sm.configuration_values))
152152

153153
>>> asyncio.run(initialize_sm())
154-
Traceback (most recent call last):
155-
...
156-
InvalidStateValue: There's no current state set. In async code, did you activate the initial state? (e.g., `await sm.activate_initial_state()`)
154+
[None]
157155

158156
```
159157

@@ -164,10 +162,10 @@ You can activate the initial state explicitly:
164162
>>> async def initialize_sm():
165163
... sm = AsyncStateMachine()
166164
... await sm.activate_initial_state()
167-
... print(sm.current_state)
165+
... print(list(sm.configuration_values))
168166

169167
>>> asyncio.run(initialize_sm())
170-
Initial
168+
['initial']
171169

172170
```
173171

@@ -178,10 +176,10 @@ before the event is handled:
178176
>>> async def initialize_sm():
179177
... sm = AsyncStateMachine()
180178
... await sm.keep() # first event activates the initial state before the event is handled
181-
... print(sm.current_state)
179+
... print(list(sm.configuration_values))
182180

183181
>>> asyncio.run(initialize_sm())
184-
Initial
182+
['initial']
185183

186184
```
187185

@@ -205,7 +203,7 @@ async def run():
205203
### Async-specific limitations
206204

207205
- **Initial state activation**: In async code, you must `await sm.activate_initial_state()`
208-
before inspecting `sm.configuration` or `sm.current_state`. In sync code this happens
206+
before inspecting `sm.configuration`. In sync code this happens
209207
automatically at instantiation time.
210208
- **Delayed events**: Both sync and async engines support `delay=` on `send()`. The async
211209
engine uses `asyncio.sleep()` internally, so it integrates naturally with event loops.

docs/diagram.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Diagrams
22

3-
You can generate diagrams from your {ref}`StateMachine`.
3+
You can generate diagrams from your {ref}`StateChart`.
44

55
```{note}
66
This functionality depends on [pydot](https://github.com/pydot/pydot), it means that you need to
@@ -116,7 +116,7 @@ usage: diagram.py [OPTION] <classpath> <out>
116116
Generate diagrams for StateMachine classes.
117117

118118
positional arguments:
119-
classpath A fully-qualified dotted path to the StateMachine class.
119+
classpath A fully-qualified dotted path to the StateChart class.
120120
out File to generate the image using extension as the output format.
121121

122122
optional arguments:

docs/guards.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ A conditional transition occurs only if specific conditions or criteria are met.
2222

2323
When a transition is conditional, it includes a condition (also known as a _guard_) that must be satisfied for the transition to take place. If the condition is not met, the transition does not occur, and the state machine remains in its current state or follows an alternative path.
2424

25-
This feature allows for multiple transitions on the same {ref}`event`, with each {ref}`transition` checked in **declaration order** — that is, the order in which the transitions themselves were created using `state.to()`. A condition acts like a predicate (a function that evaluates to true/false) and is checked when a {ref}`statemachine` handles an {ref}`event` with a transition from the current state bound to this event. The first transition that meets the conditions (if any) is executed. If none of the transitions meet the conditions, the state machine either raises an exception or does nothing (see the `allow_event_without_transition` parameter of {ref}`StateMachine`).
25+
This feature allows for multiple transitions on the same {ref}`event`, with each {ref}`transition` checked in **declaration order** — that is, the order in which the transitions themselves were created using `state.to()`. A condition acts like a predicate (a function that evaluates to true/false) and is checked when a {ref}`statemachine` handles an {ref}`event` with a transition from the current state bound to this event. The first transition that meets the conditions (if any) is executed. If none of the transitions meet the conditions, the state machine either raises an exception or does nothing (see the `allow_event_without_transition` attribute of {ref}`StateChart`).
2626

2727
````{important}
2828
**Evaluation order is based on declaration order, not composition order.**
@@ -161,18 +161,18 @@ So, a condition `s1.to(s2, cond=lambda: [])` will evaluate as `False`, as an emp
161161

162162
### Checking enabled events
163163

164-
The {ref}`StateMachine.allowed_events` property returns events reachable from the current state,
164+
The {ref}`StateChart.allowed_events` property returns events reachable from the current state,
165165
but it does **not** evaluate `cond`/`unless` guards. To check which events actually have their
166-
conditions satisfied, use {ref}`StateMachine.enabled_events`.
166+
conditions satisfied, use {ref}`StateChart.enabled_events`.
167167

168168
```{testsetup}
169169
170-
>>> from statemachine import StateMachine, State
170+
>>> from statemachine import StateChart, State
171171
172172
```
173173

174174
```py
175-
>>> class ApprovalMachine(StateMachine):
175+
>>> class ApprovalMachine(StateChart):
176176
... pending = State(initial=True)
177177
... approved = State(final=True)
178178
... rejected = State(final=True)
@@ -202,7 +202,7 @@ arguments. Any `*args`/`**kwargs` passed to `enabled_events()` are forwarded to
202202
condition callbacks, just like when triggering an event:
203203

204204
```py
205-
>>> class TaskMachine(StateMachine):
205+
>>> class TaskMachine(StateChart):
206206
... idle = State(initial=True)
207207
... running = State(final=True)
208208
...

docs/integrations.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ When used in a Django App, this library implements an auto-discovery hook simila
77
built-in **admin** [autodiscover](https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.autodiscover).
88

99
> This library attempts to import an **statemachine** or **statemachines** module in each installed
10-
> application. Such modules are expected to register `StateMachine` classes to be used with
10+
> application. Such modules are expected to register `StateChart` classes to be used with
1111
> the {ref}`MachineMixin`.
1212
1313

1414
```{hint}
1515
When using `python-statemachine` to control the state of a Django model, we advise keeping the
16-
{ref}`StateMachine` definitions on their own modules.
16+
{ref}`StateChart` definitions on their own modules.
1717
1818
So as circular references may occur, and as a way to help you organize your
1919
code, if you put state machines on modules named as mentioned above inside installed
20-
Django Apps, these {ref}`StateMachine` classes will be automatically
20+
Django Apps, these {ref}`StateChart` classes will be automatically
2121
imported and registered.
2222
2323
This is only an advice, nothing stops you do declare your state machine alongside your models.
@@ -31,12 +31,12 @@ Given this StateMachine:
3131
```py
3232
# campaign/statemachines.py
3333

34-
from statemachine import StateMachine
34+
from statemachine import StateChart
3535
from statemachine import State
3636
from statemachine.mixins import MachineMixin
3737

3838

39-
class CampaignMachineWithKeys(StateMachine):
39+
class CampaignMachineWithKeys(StateChart):
4040
"A workflow machine"
4141
draft = State('Draft', initial=True, value=1)
4242
producing = State('Being produced', value=2)

0 commit comments

Comments
 (0)