Skip to content

Commit 2420b18

Browse files
authored
docs: Improve 2.0 release notes and readme (#371)
1 parent c9a7215 commit 2420b18

5 files changed

Lines changed: 211 additions & 40 deletions

File tree

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: 2 additions & 2 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
12.4 KB
Loading

docs/releases/2.0.0.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,138 @@ See {ref}`Add a translation` on how to contribute with translations.
140140
- Dropped support for Django <= `1.6` for auto-discovering and registering `StateMachine` classes
141141
to be used on {ref}`django integration`.
142142

143+
#### Transitions with multiple events only execute actions associated to the triggered event
144+
145+
Prior to [#365](https://github.com/fgmacedo/python-statemachine/pull/365), when you {ref}`Declare transition actions by naming convention`, all callbacks of the transition were called even if the triggered event was not the one that originated the transition.
146+
147+
This behavior was fixed in this release. Now, only the transitions associated with the triggered event or directly assigned to the transition are called.
148+
149+
Consider the following state machine as an example:
150+
151+
```py
152+
>>> from statemachine import State
153+
>>> from statemachine import StateMachine
154+
155+
>>> class TrafficLightMachine(StateMachine):
156+
... "A traffic light machine"
157+
... green = State(initial=True)
158+
... yellow = State()
159+
... red = State()
160+
...
161+
... slowdown = green.to(yellow)
162+
... stop = yellow.to(red)
163+
... go = red.to(green)
164+
...
165+
... cycle = slowdown | stop | go
166+
...
167+
... def before_slowdown(self):
168+
... print("Slowdown")
169+
...
170+
... def before_cycle(self, event: str, source: State, target: State):
171+
... print(f"Running {event} from {source.id} to {target.id}")
172+
173+
```
174+
175+
Before, if you send the `cycle` event, the behavior was to also trigger actions associated with
176+
`slowdown`, because they're sharing the same instance of {ref}`Transition`:
177+
178+
```py
179+
>>> sm = TrafficLightMachine()
180+
>>> sm.send("cycle") # doctest: +SKIP
181+
Slowdown
182+
Running cycle from green to yellow
183+
184+
```
185+
186+
Now the behavior is to only execute actions bound to the triggered {ref}`event` or directly
187+
associated to the {ref}`Transition`:
188+
189+
```py
190+
>>> sm = TrafficLightMachine()
191+
>>> sm.send("cycle")
192+
Running cycle from green to yellow
193+
194+
```
195+
196+
If you want to emulate the previous behavior, consider one of these alternatives.
197+
198+
You can {ref}`Bind transition actions using params` or {ref}`Bind transition actions using decorator syntax`:
199+
200+
```py
201+
>>> from statemachine import State
202+
>>> from statemachine import StateMachine
203+
204+
>>> class TrafficLightMachine(StateMachine):
205+
... "A traffic light machine"
206+
... green = State(initial=True)
207+
... yellow = State()
208+
... red = State()
209+
...
210+
... slowdown = green.to(yellow, before="do_before_slowdown") # assign to the transition
211+
... stop = yellow.to(red)
212+
... go = red.to(green)
213+
...
214+
... cycle = slowdown | stop | go
215+
...
216+
... def do_before_slowdown(self):
217+
... print("Slowdown")
218+
...
219+
... @stop.before # assign to the transition
220+
... def do_before_stop(self):
221+
... print("Stop")
222+
...
223+
... def before_cycle(self, event: str, source: State, target: State):
224+
... print(f"Running {event} from {source.id} to {target.id}")
225+
226+
```
227+
228+
You can go an step further and if the events are not called externally, get rid of them and put the actions directly on the transitions:
229+
230+
231+
```py
232+
>>> from statemachine import State
233+
>>> from statemachine import StateMachine
234+
235+
>>> class TrafficLightMachine(StateMachine):
236+
... "A traffic light machine"
237+
... green = State(initial=True)
238+
... yellow = State()
239+
... red = State()
240+
...
241+
... cycle = (
242+
... green.to(yellow, before="slowdown")
243+
... | yellow.to(red, before="stop")
244+
... | red.to(green, before="go")
245+
... )
246+
...
247+
... def slowdown(self):
248+
... print("Slowdown")
249+
...
250+
... def stop(self):
251+
... print("Stop")
252+
...
253+
... def go(self):
254+
... print("Go")
255+
...
256+
... def before_cycle(self, event: str, source: State, target: State):
257+
... print(f"Running {event} from {source.id} to {target.id}")
258+
259+
```
260+
261+
```py
262+
>>> sm = TrafficLightMachine()
263+
>>> [sm.send("cycle") for _ in range(3)]
264+
Slowdown
265+
Running cycle from green to yellow
266+
Stop
267+
Running cycle from yellow to red
268+
Go
269+
Running cycle from red to green
270+
[[None, None], [None, None], [None, None]]
271+
272+
```
273+
274+
143275
### Statemachine class changes in 2.0
144276

145277
#### The new processing model (RTC) by default

docs/releases/2.0.1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ StateMachine 2.0.1 is a bugfix release.
88

99
## Bugfixes
1010

11-
- Fixes [#369](https://github.com/fgmacedo/python-statemachine/issues/336) adding support to wrap
11+
- Fixes [#369](https://github.com/fgmacedo/python-statemachine/issues/369) adding support to wrap
1212
methods used as {ref}`Actions` decorated with `functools.partial`.

0 commit comments

Comments
 (0)