Skip to content

Commit f95391a

Browse files
committed
Merge branch 'release/2.1.0'
2 parents 2ae3223 + 3a16db1 commit f95391a

30 files changed

+1216
-1498
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# This workflow was added by CodeSee. Learn more at https://codesee.io/
2+
# This is v2.0 of this workflow file
3+
on:
4+
push:
5+
branches:
6+
- develop
7+
pull_request_target:
8+
types: [opened, synchronize, reopened]
9+
10+
name: CodeSee
11+
12+
permissions: read-all
13+
14+
jobs:
15+
codesee:
16+
runs-on: ubuntu-latest
17+
continue-on-error: true
18+
name: Analyze the repo with CodeSee
19+
steps:
20+
- uses: Codesee-io/codesee-action@v2
21+
with:
22+
codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }}
23+
codesee-url: https://app.codesee.io

.pre-commit-config.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ repos:
99
exclude: docs/auto_examples
1010
- repo: https://github.com/charliermarsh/ruff-pre-commit
1111
# Ruff version.
12-
rev: 'v0.0.220'
12+
rev: 'v0.0.257'
1313
hooks:
1414
- id: ruff
15-
# Respect `exclude` and `extend-exclude` settings.
16-
args: ["--force-exclude"]
1715
- repo: https://github.com/psf/black
1816
rev: 22.10.0
1917
hooks:

README.md

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,11 @@ Define your state machine:
5959
... yellow = State()
6060
... red = State()
6161
...
62-
... cycle = green.to(yellow) | yellow.to(red) | red.to(green)
63-
...
64-
... slowdown = green.to(yellow)
65-
... stop = yellow.to(red)
66-
... go = red.to(green)
62+
... cycle = (
63+
... green.to(yellow)
64+
... | yellow.to(red)
65+
... | red.to(green)
66+
... )
6767
...
6868
... def before_cycle(self, event: str, source: State, target: State, message: str = ""):
6969
... message = ". " + message if message else ""
@@ -80,124 +80,163 @@ Define your state machine:
8080
You can now create an instance:
8181

8282
```py
83-
>>> traffic_light = TrafficLightMachine()
83+
>>> sm = TrafficLightMachine()
8484

8585
```
8686

87-
Then start sending events:
87+
This state machine can be represented graphically as follows:
8888

8989
```py
90-
>>> traffic_light.cycle()
91-
'Running cycle from green to yellow'
90+
>>> img_path = "docs/images/readme_trafficlightmachine.png"
91+
>>> sm._graph().write_png(img_path)
9292

9393
```
9494

95-
You can inspect the current state:
95+
![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png)
96+
97+
98+
Where on the `TrafficLightMachine`, we've defined `green`, `yellow`, and `red` as states, and
99+
one event called `cycle`, which is bound to the transitions from `green` to `yellow`, `yellow` to `red`,
100+
and `red` to `green`. We also have defined three callbacks by name convention, `before_cycle`, `on_enter_red`, and `on_exit_red`.
101+
102+
103+
Then start sending events to your new state machine:
96104

97105
```py
98-
>>> traffic_light.current_state.id
99-
'yellow'
106+
>>> sm.send("cycle")
107+
'Running cycle from green to yellow'
100108

101109
```
102110

103-
A `State` human-readable name is automatically derived from the `State.id`:
111+
That's it. This is all an external object needs to know about your state machine: How to send events.
112+
Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.
113+
114+
But if your use case needs, you can inspect state machine properties, like the current state:
104115

105116
```py
106-
>>> traffic_light.current_state.name
107-
'Yellow'
117+
>>> sm.current_state.id
118+
'yellow'
108119

109120
```
110121

111122
Or get a complete state representation for debugging purposes:
112123

113124
```py
114-
>>> traffic_light.current_state
125+
>>> sm.current_state
115126
State('Yellow', id='yellow', value='yellow', initial=False, final=False)
116127

117128
```
118129

119-
The ``State`` instance can also be checked by equality:
130+
The `State` instance can also be checked by equality:
120131

121132
```py
122-
>>> traffic_light.current_state == TrafficLightMachine.yellow
133+
>>> sm.current_state == TrafficLightMachine.yellow
123134
True
124135

125-
>>> traffic_light.current_state == traffic_light.yellow
136+
>>> sm.current_state == sm.yellow
126137
True
127138

128139
```
129140

130-
But for your convenience, can easily ask if a state is active at any time:
141+
Or you can check if a state is active at any time:
131142

132143
```py
133-
>>> traffic_light.green.is_active
144+
>>> sm.green.is_active
134145
False
135146

136-
>>> traffic_light.yellow.is_active
147+
>>> sm.yellow.is_active
137148
True
138149

139-
>>> traffic_light.red.is_active
150+
>>> sm.red.is_active
140151
False
141152

142153
```
143154

144155
Easily iterate over all states:
145156

146157
```py
147-
>>> [s.id for s in traffic_light.states]
158+
>>> [s.id for s in sm.states]
148159
['green', 'red', 'yellow']
149160

150161
```
151162

152163
Or over events:
153164

154165
```py
155-
>>> [t.name for t in traffic_light.events]
156-
['cycle', 'go', 'slowdown', 'stop']
166+
>>> [t.name for t in sm.events]
167+
['cycle']
157168

158169
```
159170

160171
Call an event by its name:
161172

162173
```py
163-
>>> traffic_light.cycle()
174+
>>> sm.cycle()
164175
Don't move.
165176
'Running cycle from yellow to red'
166177

167178
```
168179
Or send an event with the event name:
169180

170181
```py
171-
>>> traffic_light.send('cycle')
182+
>>> sm.send('cycle')
172183
Go ahead!
173184
'Running cycle from red to green'
174185

175-
>>> traffic_light.green.is_active
186+
>>> sm.green.is_active
176187
True
177188

178189
```
179-
You can't run a transition from an invalid state:
190+
191+
You can pass arbitrary positional or keyword arguments to the event, and
192+
they will be propagated to all actions and callbacks using something similar to dependency injection. In other words, the library will only inject the parameters declared on the
193+
callback method.
194+
195+
Note how `before_cycle` was declared:
180196

181197
```py
182-
>>> traffic_light.go()
198+
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
199+
message = ". " + message if message else ""
200+
return f"Running {event} from {source.id} to {target.id}{message}"
201+
```
202+
203+
The params `event`, `source`, `target` (and others) are available built-in to be used on any action.
204+
The param `message` is user-defined, in our example we made it default empty so we can call `cycle` with
205+
or without a `message` parameter.
206+
207+
If we pass a `message` parameter, it will be used on the `before_cycle` action:
208+
209+
```py
210+
>>> sm.send("cycle", message="Please, now slowdown.")
211+
'Running cycle from green to yellow. Please, now slowdown.'
212+
213+
```
214+
215+
216+
By default, events with transitions that cannot run from the current state or unknown events
217+
raise a `TransitionNotAllowed` exception:
218+
219+
```py
220+
>>> sm.send("go")
183221
Traceback (most recent call last):
184-
statemachine.exceptions.TransitionNotAllowed: Can't go when in Green.
222+
statemachine.exceptions.TransitionNotAllowed: Can't go when in Yellow.
185223

186224
```
225+
187226
Keeping the same state as expected:
188227

189228
```py
190-
>>> traffic_light.green.is_active
229+
>>> sm.yellow.is_active
191230
True
192231

193232
```
194233

195-
And you can pass arbitrary positional or keyword arguments to the event, and
196-
they will be propagated to all actions and callbacks:
234+
A human-readable name is automatically derived from the `State.id`, which is used on the messages
235+
and in diagrams:
197236

198237
```py
199-
>>> traffic_light.cycle(message="Please, now slowdown.")
200-
'Running cycle from green to yellow. Please, now slowdown.'
238+
>>> sm.current_state.name
239+
'Yellow'
201240

202241
```
203242

docs/actions.md

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,9 @@ It's also possible to use an event name as action to chain transitions.
225225
**Be careful to not introduce recursion errors**, like `loop = initial.to.itself(after="loop")`, that will raise `RecursionError` exception.
226226
```
227227

228-
### Bind event actions using decorator syntax
228+
### Bind transition actions using decorator syntax
229229

230-
The action will be registered for every {ref}`transition` associated with the event.
230+
The action will be registered for every {ref}`transition` in the list associated with the event.
231231

232232

233233
```py
@@ -333,6 +333,48 @@ Actions and Guards will be executed in the following order:
333333
- `after_transition()`
334334

335335

336+
## Return values
337+
338+
Currently only certain actions' return values will be combined as a list and returned for
339+
a triggered transition:
340+
341+
- `before_transition()`
342+
343+
- `before_<event>()`
344+
345+
- `on_transition()`
346+
347+
- `on_<event>()`
348+
349+
Note that `None` will be used if the action callback does not return anything, but only when it is
350+
defined explicitly. The following provides an example:
351+
352+
```py
353+
>>> class ExampleStateMachine(StateMachine):
354+
... initial = State(initial=True)
355+
...
356+
... loop = initial.to.itself()
357+
...
358+
... def before_loop(self):
359+
... return "Before loop"
360+
...
361+
... def on_transition(self):
362+
... pass
363+
...
364+
... def on_loop(self):
365+
... return "On loop"
366+
...
367+
368+
>>> sm = ExampleStateMachine()
369+
370+
>>> sm.loop()
371+
['Before loop', None, 'On loop']
372+
373+
```
374+
375+
For {ref}`RTC model`, only the main event will get its value list, while the chained ones simply get
376+
`None` returned. For {ref}`Non-RTC model`, results for every event will always be collected and returned.
377+
336378

337379
(dynamic-dispatch)=
338380
## Dynamic dispatch
@@ -341,7 +383,7 @@ Actions and Guards will be executed in the following order:
341383
Guards. This means that you can declare an arbitrary number of `*args` and `**kwargs`, and the
342384
library will match your method signature of what's expected to receive with the provided arguments.
343385

344-
This means that if on your `on_enter_<state.id>()` or `on_execute_<event>()` method, you need to know
386+
This means that if on your `on_enter_<state.id>()` or `on_<event>()` method, you need to know
345387
the `source` ({ref}`state`), or the `event` ({ref}`event`), or access a keyword
346388
argument passed with the trigger, just add this parameter to the method and It will be passed
347389
by the dispatch mechanics.

docs/api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@
2020
:members:
2121
```
2222

23+
## States (class)
24+
25+
```{eval-rst}
26+
.. autoclass:: statemachine.states.States
27+
:noindex:
28+
:members:
29+
```
30+
2331
## Transition
2432

2533
```{seealso}
12.4 KB
Loading

0 commit comments

Comments
 (0)