Skip to content

Commit 4c7e938

Browse files
authored
feat: Actions as decorators (#307)
1 parent be9ec1e commit 4c7e938

20 files changed

Lines changed: 620 additions & 176 deletions

docs/_static/custom_machine.css

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,30 @@
33
visibility: hidden;
44
} */
55

6-
.sphx-glr-thumbnails {
7-
grid-template-columns: repeat(auto-fill, minmax(600px, 1fr)) !important;
8-
}
6+
@media only screen and (min-width: 650px) {
97

10-
.sphx-glr-thumbcontainer {
11-
min-height: 320px !important;
12-
margin: 20px !important;
13-
justify-content: center;
14-
}
15-
.sphx-glr-thumbcontainer .figure {
16-
width: 600px !important;
17-
}
18-
.sphx-glr-thumbcontainer img {
19-
max-height: 250px !important;
20-
max-width: 600px !important;
21-
width: 100% !important;
22-
}
23-
.sphx-glr-thumbcontainer a.internal {
24-
padding: 20px 10px 0 !important;
25-
}
8+
.sphx-glr-thumbnails {
9+
grid-template-columns: repeat(auto-fill, minmax(600px, 1fr)) !important;
10+
}
11+
12+
.sphx-glr-thumbcontainer {
13+
min-height: 320px !important;
14+
margin: 20px !important;
15+
justify-content: center;
16+
}
17+
.sphx-glr-thumbcontainer .figure {
18+
width: 600px !important;
19+
}
20+
.sphx-glr-thumbcontainer img {
21+
max-height: 250px !important;
22+
max-width: 600px !important;
23+
width: 100% !important;
24+
}
25+
.sphx-glr-thumbcontainer a.internal {
26+
padding: 20px 10px 0 !important;
27+
}
2628

29+
}
2730

2831
/* Gallery Donwload buttons */
2932
div.sphx-glr-download a {

docs/actions.md

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

3-
4-
An action is the way a statemachine can cause things to happen in the
3+
An action is the way a StateMachine can cause things to happen in the
54
outside world, and indeed they are the main reason why they exist at all.
5+
66
The main point of introducing a state machine is for the
77
actions to be invoked at the right times, depending on the sequence of events
88
and the state of the guards.
@@ -11,37 +11,266 @@ 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-
machine in execution.
14+
StateMachine 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 bounded to a specific state or event:
1818

19-
- `before_transition(event_data)`
19+
- `before_transition()`
20+
21+
- `on_exit_state()`
22+
23+
- `on_transition()`
24+
25+
- `on_enter_state()`
26+
27+
- `after_transition()`
2028

21-
- `on_enter_state(event_data)`
29+
The follow example can get you an overview of the "generic" callbacks available:
2230

23-
- `on_exit_state(event_data)`
31+
```py
32+
>>> from statemachine import StateMachine, State
33+
34+
>>> class ExampleStateMachine(StateMachine):
35+
... initial = State("Initial", initial=True)
36+
... final = State("Final", final=True)
37+
...
38+
... loop = initial.to.itself()
39+
... go = initial.to(final)
40+
...
41+
... def before_transition(self, event, state):
42+
... print("Before '{}', on the '{}' state.".format(event, state.id))
43+
... return "before_transition_return"
44+
...
45+
... def on_transition(self, event, state):
46+
... print("On '{}', on the '{}' state.".format(event, state.id))
47+
... return "on_transition_return"
48+
...
49+
... def on_exit_state(self, event, state):
50+
... print("Exiting '{}' state from '{}' event.".format(state.id, event))
51+
...
52+
... def on_enter_state(self, event, state):
53+
... print("Entering '{}' state from '{}' event.".format(state.id, event))
54+
...
55+
... def after_transition(self, event, state):
56+
... print("After '{}', on the '{}' state.".format(event, state.id))
57+
58+
59+
>>> sm = ExampleStateMachine() # On initialization, the machine run a special event `__initial__`
60+
Entering 'initial' state from '__initial__' event.
61+
62+
>>> sm.loop()
63+
Before 'loop', on the 'initial' state.
64+
Exiting 'initial' state from 'loop' event.
65+
On 'loop', on the 'initial' state.
66+
Entering 'initial' state from 'loop' event.
67+
After 'loop', on the 'initial' state.
68+
['before_transition_return', 'on_transition_return']
69+
70+
>>> sm.go()
71+
Before 'go', on the 'initial' state.
72+
Exiting 'initial' state from 'go' event.
73+
On 'go', on the 'initial' state.
74+
Entering 'final' state from 'go' event.
75+
After 'go', on the 'final' state.
76+
['before_transition_return', 'on_transition_return']
77+
78+
```
2479

25-
- `after_transition(event_data)`
80+
81+
```{seealso}
82+
All actions and {ref}`guards` support multiple method signatures. They follow the
83+
{ref}`dynamic-dispatch` method calling implemented on this library.
84+
```
2685

2786
## State actions
2887

29-
For each defined state, you can register `on_enter_<state>` and `on_exit_<state>` callbacks.
88+
For each defined {ref}`state`, you can declare `enter` and `exit` callbacks.
89+
90+
### Declare state actions by name convention
91+
92+
Callbacks by name convention will be searched on the StateMachine and on the
93+
model, using the patterns:
94+
95+
- `on_enter_<state.id>()`
96+
97+
- `on_exit_<state.id>()`
98+
99+
100+
```py
101+
>>> from statemachine import StateMachine, State
102+
103+
>>> class ExampleStateMachine(StateMachine):
104+
... initial = State("Initial", initial=True)
105+
...
106+
... loop = initial.to.itself()
107+
...
108+
... def on_enter_initial(self):
109+
... pass
110+
...
111+
... def on_exit_initial(self):
112+
... pass
113+
114+
```
115+
116+
### Bind state actions using params
117+
118+
Use the `enter` or `exit` params available on the `State` constructor.
119+
120+
```py
121+
>>> from statemachine import StateMachine, State
122+
123+
>>> class ExampleStateMachine(StateMachine):
124+
... initial = State("Initial", initial=True, enter="entering_initial", exit="leaving_initial")
125+
...
126+
... loop = initial.to.itself()
127+
...
128+
... def entering_initial(self):
129+
... pass
130+
...
131+
... def leaving_initial(self):
132+
... pass
133+
134+
```
135+
136+
### Bind state actions using decorator syntax
137+
138+
139+
```py
140+
>>> from statemachine import StateMachine, State
141+
142+
>>> class ExampleStateMachine(StateMachine):
143+
... initial = State("Initial", initial=True)
144+
...
145+
... loop = initial.to.itself()
146+
...
147+
... @initial.enter
148+
... def entering_initial(self):
149+
... pass
150+
...
151+
... @initial.exit
152+
... def leaving_initial(self):
153+
... pass
154+
155+
```
156+
157+
## Transition actions
158+
159+
For each {ref}`event`, you can register `before`, `on` and `after` callbacks.
160+
161+
### Declare transition actions by name convention
162+
163+
The action will be registered for every {ref}`transition` associated with the event.
164+
165+
Callbacks by name convention will be searched on the StateMachine and on the
166+
model, using the patterns:
167+
168+
- `before_<event>()`
169+
170+
- `on_<event>()`
171+
172+
- `after_<event>()`
173+
174+
175+
```py
176+
>>> from statemachine import StateMachine, State
177+
178+
>>> class ExampleStateMachine(StateMachine):
179+
... initial = State("Initial", initial=True)
180+
...
181+
... loop = initial.to.itself()
182+
...
183+
... def before_loop(self):
184+
... pass
185+
...
186+
... def on_loop(self):
187+
... pass
188+
...
189+
... def after_loop(self):
190+
... pass
191+
...
192+
193+
```
194+
195+
### Bind transition actions using params
196+
197+
```py
198+
>>> from statemachine import StateMachine, State
199+
200+
>>> class ExampleStateMachine(StateMachine):
201+
... initial = State("Initial", initial=True)
202+
...
203+
... loop = initial.to.itself(before="just_before", on="its_happening", after="loop_completed")
204+
...
205+
... def just_before(self):
206+
... pass
207+
...
208+
... def its_happening(self):
209+
... pass
210+
...
211+
... def loop_completed(self):
212+
... pass
213+
214+
```
215+
216+
### Bind event actions using decorator syntax
217+
218+
The action will be registered for every {ref}`transition` associated with the event.
219+
220+
221+
```py
222+
>>> from statemachine import StateMachine, State
223+
224+
>>> class ExampleStateMachine(StateMachine):
225+
... initial = State("Initial", initial=True)
226+
...
227+
... loop = initial.to.itself()
228+
...
229+
... @loop.before
230+
... def just_before(self):
231+
... pass
232+
...
233+
... @loop.on
234+
... def its_happening(self):
235+
... pass
236+
...
237+
... @loop.after
238+
... def loop_completed(self):
239+
... pass
30240

31-
- `on_enter_<state_identifier>(event_data)`
241+
```
32242

33-
- `on_exit_<state_identifier>(event_data)`
243+
### Declare an event while also giving an "on" action using the decorator syntax
34244

245+
You can also declare an event while also adding a callback:
35246

36-
The initial {ref}`state` is entered when the machine starts and the corresponding actions `on_enter_state` or `on_enter_<state>` are called if defined.
247+
```py
248+
>>> from statemachine import StateMachine, State
37249

38-
## Event actions
250+
>>> class ExampleStateMachine(StateMachine):
251+
... initial = State("Initial", initial=True)
252+
...
253+
... @initial.to.itself()
254+
... def loop(self):
255+
... print("On loop")
256+
... return 42
257+
258+
```
39259

40-
For each event, you can register `before_<event>` and `after_<event>`
260+
Note that with this syntax, the result `loop` that is present on the `ExampleStateMachine.loop`
261+
namespacte is not a simple method, but an {ref}`event` trigger. So it only executes if the
262+
StateMachine is on the right state.
41263

42-
- `before_<event>(event_data)`
264+
So, you can use the event-oriented approach:
43265

44-
- `after_<event>(event_data)`
266+
```py
267+
>>> sm = ExampleStateMachine()
268+
269+
>>> sm.send("loop")
270+
On loop
271+
42
272+
273+
```
45274

46275

47276
## Other callbacks
@@ -93,7 +322,7 @@ python-statemachine implements a custom dispatch mechanism on all those availabl
93322
Guards, this means that you can declare an arbitrary number of `*args` and `**kwargs`, and the
94323
library will match your method signature of what's expect to receive with the provided arguments.
95324

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

docs/auto_examples/all_actions_machine.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
},
2727
"outputs": [],
2828
"source": [
29-
"import mock\n\nfrom statemachine import StateMachine, State\n\n\nclass AllActionsMachine(StateMachine):\n\n initial = State(\"Initial\", initial=True)\n final = State(\"Final\", final=True)\n\n go = initial.to(\n final,\n validators=[\"validation_1\", \"validation_2\"],\n cond=[\"condition_1\", \"condition_2\"],\n unless=[\"unless_1\", \"unless_2\"],\n on_execute=[\"on_execute_1\", \"on_execute_2\"],\n before=[\"before_go_inline_1\", \"before_go_inline_2\"],\n after=[\"after_go_inline_1\", \"after_go_inline_2\"],\n )\n\n def __init__(self, *args, **kwargs):\n self.spy = mock.Mock(side_effect=lambda x: x)\n super(AllActionsMachine, self).__init__(*args, **kwargs)\n\n # validators and guards\n\n def validation_1(self):\n # this method may raise an exception\n return self.spy(\"validation_1\")\n\n def validation_2(self):\n # this method may raise an exception\n return self.spy(\"validation_2\")\n\n def condition_1(self):\n self.spy(\"condition_1\")\n return True\n\n def condition_2(self):\n self.spy(\"condition_2\")\n return True\n\n def unless_1(self):\n self.spy(\"unless_1\")\n return False\n\n def unless_2(self):\n self.spy(\"unless_2\")\n return False\n\n # generics state\n\n def on_enter_state(self):\n return self.spy(\"on_enter_state\")\n\n def on_exit_state(self):\n return self.spy(\"on_exit_state\")\n\n # generics transition\n\n def before_transition(self):\n return self.spy(\"before_transition\")\n\n def on_transition(self):\n return self.spy(\"on_transition\")\n\n def after_transition(self):\n return self.spy(\"after_transition\")\n\n # before / after specific\n\n def before_go_inline_1(self):\n return self.spy(\"before_go_inline_1\")\n\n def before_go_inline_2(self):\n return self.spy(\"before_go_inline_2\")\n\n def before_go(self):\n return self.spy(\"before_go\")\n\n def on_execute_1(self):\n return self.spy(\"on_execute_1\")\n\n def on_execute_2(self):\n return self.spy(\"on_execute_2\")\n\n def on_go(self):\n return self.spy(\"on_go\")\n\n def after_go_inline_1(self):\n return self.spy(\"after_go_inline_1\")\n\n def after_go_inline_2(self):\n return self.spy(\"after_go_inline_2\")\n\n def after_go(self):\n return self.spy(\"after_go\")\n\n # enter / exit specific\n\n def on_enter_initial(self):\n return self.spy(\"on_enter_initial\")\n\n def on_exit_initial(self):\n return self.spy(\"on_exit_initial\")\n\n def on_enter_final(self):\n return self.spy(\"on_enter_final\")\n\n def on_exit_final(self):\n \"hopefully this will not be called\"\n return self.spy(\"on_exit_final\")"
29+
"import mock\n\nfrom statemachine import StateMachine, State\n\n\nclass AllActionsMachine(StateMachine):\n\n initial = State(\"Initial\", initial=True)\n final = State(\"Final\", final=True)\n\n go = initial.to(\n final,\n validators=[\"validation_1\", \"validation_2\"],\n cond=[\"condition_1\", \"condition_2\"],\n unless=[\"unless_1\", \"unless_2\"],\n on=[\"on_inline_1\", \"on_inline_2\"],\n before=[\"before_go_inline_1\", \"before_go_inline_2\"],\n after=[\"after_go_inline_1\", \"after_go_inline_2\"],\n )\n\n def __init__(self, *args, **kwargs):\n self.spy = mock.Mock(side_effect=lambda x: x)\n super(AllActionsMachine, self).__init__(*args, **kwargs)\n\n # validators and guards\n\n def validation_1(self):\n # this method may raise an exception\n return self.spy(\"validation_1\")\n\n def validation_2(self):\n # this method may raise an exception\n return self.spy(\"validation_2\")\n\n def condition_1(self):\n self.spy(\"condition_1\")\n return True\n\n def condition_2(self):\n self.spy(\"condition_2\")\n return True\n\n def unless_1(self):\n self.spy(\"unless_1\")\n return False\n\n def unless_2(self):\n self.spy(\"unless_2\")\n return False\n\n # generics state\n\n def on_enter_state(self):\n return self.spy(\"on_enter_state\")\n\n def on_exit_state(self):\n return self.spy(\"on_exit_state\")\n\n # generics transition\n\n def before_transition(self):\n return self.spy(\"before_transition\")\n\n def on_transition(self):\n return self.spy(\"on_transition\")\n\n def after_transition(self):\n return self.spy(\"after_transition\")\n\n # before / after specific\n\n @go.before\n def before_go_decor(self):\n return self.spy(\"before_go_decor\")\n\n def before_go_inline_1(self):\n return self.spy(\"before_go_inline_1\")\n\n def before_go_inline_2(self):\n return self.spy(\"before_go_inline_2\")\n\n def before_go(self):\n return self.spy(\"before_go\")\n\n @go.on\n def go_on_decor(self):\n return self.spy(\"go_on_decor\")\n\n def on_inline_1(self):\n return self.spy(\"on_inline_1\")\n\n def on_inline_2(self):\n return self.spy(\"on_inline_2\")\n\n def on_go(self):\n return self.spy(\"on_go\")\n\n @go.after\n def after_go_decor(self):\n return self.spy(\"after_go_decor\")\n\n def after_go_inline_1(self):\n return self.spy(\"after_go_inline_1\")\n\n def after_go_inline_2(self):\n return self.spy(\"after_go_inline_2\")\n\n def after_go(self):\n return self.spy(\"after_go\")\n\n # enter / exit specific\n\n @initial.enter\n def enter_initial_decor(self):\n return self.spy(\"enter_initial_decor\")\n\n def on_enter_initial(self):\n return self.spy(\"on_enter_initial\")\n\n @initial.exit\n def exit_initial_decor(self):\n return self.spy(\"exit_initial_decor\")\n\n def on_exit_initial(self):\n return self.spy(\"on_exit_initial\")\n\n def on_enter_final(self):\n return self.spy(\"on_enter_final\")\n\n def on_exit_final(self):\n \"hopefully this will not be called\"\n return self.spy(\"on_exit_final\")"
3030
]
3131
},
3232
{
@@ -62,7 +62,7 @@
6262
},
6363
"outputs": [],
6464
"source": [
65-
"result = machine.go()\nassert result == [\n \"before_transition\",\n \"before_go_inline_1\",\n \"before_go_inline_2\",\n \"before_go\",\n \"on_transition\",\n \"on_execute_1\",\n \"on_execute_2\",\n \"on_go\",\n]"
65+
"result = machine.go()\nassert result == [\n \"before_transition\",\n \"before_go_inline_1\",\n \"before_go_inline_2\",\n \"before_go_decor\",\n \"before_go\",\n \"on_transition\",\n \"on_inline_1\",\n \"on_inline_2\",\n \"go_on_decor\",\n \"on_go\",\n]"
6666
]
6767
},
6868
{
@@ -80,7 +80,7 @@
8080
},
8181
"outputs": [],
8282
"source": [
83-
"assert spy.call_args_list == [\n mock.call(\"on_enter_state\"),\n mock.call(\"on_enter_initial\"),\n\n mock.call(\"validation_1\"),\n mock.call(\"validation_2\"),\n\n mock.call(\"condition_1\"),\n mock.call(\"condition_2\"),\n\n mock.call(\"unless_1\"),\n mock.call(\"unless_2\"),\n\n mock.call(\"before_transition\"),\n mock.call(\"before_go_inline_1\"),\n mock.call(\"before_go_inline_2\"),\n mock.call(\"before_go\"),\n\n mock.call(\"on_exit_state\"),\n mock.call(\"on_exit_initial\"),\n\n mock.call(\"on_transition\"),\n mock.call(\"on_execute_1\"),\n mock.call(\"on_execute_2\"),\n mock.call(\"on_go\"),\n\n mock.call(\"on_enter_state\"),\n mock.call(\"on_enter_final\"),\n\n mock.call(\"after_go_inline_1\"),\n mock.call(\"after_go_inline_2\"),\n mock.call(\"after_go\"),\n mock.call(\"after_transition\"),\n]"
83+
"assert spy.call_args_list == [\n mock.call(\"on_enter_state\"),\n mock.call(\"enter_initial_decor\"),\n mock.call(\"on_enter_initial\"),\n\n mock.call(\"validation_1\"),\n mock.call(\"validation_2\"),\n\n mock.call(\"condition_1\"),\n mock.call(\"condition_2\"),\n\n mock.call(\"unless_1\"),\n mock.call(\"unless_2\"),\n\n mock.call(\"before_transition\"),\n mock.call(\"before_go_inline_1\"),\n mock.call(\"before_go_inline_2\"),\n mock.call(\"before_go_decor\"),\n mock.call(\"before_go\"),\n\n mock.call(\"on_exit_state\"),\n mock.call(\"exit_initial_decor\"),\n mock.call(\"on_exit_initial\"),\n\n mock.call(\"on_transition\"),\n mock.call(\"on_inline_1\"),\n mock.call(\"on_inline_2\"),\n mock.call(\"go_on_decor\"),\n mock.call(\"on_go\"),\n\n mock.call(\"on_enter_state\"),\n mock.call(\"on_enter_final\"),\n\n mock.call(\"after_go_inline_1\"),\n mock.call(\"after_go_inline_2\"),\n mock.call(\"after_go_decor\"),\n mock.call(\"after_go\"),\n mock.call(\"after_transition\"),\n]"
8484
]
8585
}
8686
],

0 commit comments

Comments
 (0)