Skip to content

Commit b9e3fca

Browse files
authored
fix: Transition with multiple events was calling actions of all events (#365)
1 parent e8c1feb commit b9e3fca

8 files changed

Lines changed: 129 additions & 44 deletions

File tree

docs/releases/2.0.0.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ See {ref}`Add a translation` on how to contribute with translations.
131131

132132
- [#341](https://github.com/fgmacedo/python-statemachine/issues/341): Fix dynamic dispatch
133133
on methods with default parameters.
134+
- [#365](https://github.com/fgmacedo/python-statemachine/pull/365): Fix transition with multiple
135+
events was calling actions of all events.
134136

135137

136138
## Backward incompatible changes in 2.0
@@ -185,7 +187,7 @@ Should become:
185187
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
186188

187189
>>> sm = TrafficLightMachine()
188-
>>> assert [t.name for t in sm.allowed_events] == ["cycle"]
190+
>>> assert [t.name for t in sm.allowed_events] == ["cycle", "slowdown"]
189191

190192
```
191193

docs/transitions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ Or in an event-oriented style, events are `send`:
199199

200200
```py
201201
>>> machine.send("cycle")
202+
Don't move.
202203
'Running cycle from yellow to red'
203204

204205
>>> machine.current_state.id
@@ -226,6 +227,7 @@ can also raise an exception at this point to stop a transition to occur.
226227
'red'
227228

228229
>>> machine.cycle()
230+
Go ahead!
229231
'Running cycle from red to green'
230232

231233
>>> machine.current_state.id

statemachine/callbacks.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ class CallbackWrapper:
1414
call is performed, to allow the proper callback resolution.
1515
"""
1616

17-
def __init__(self, func, suppress_errors=False):
17+
def __init__(self, func, suppress_errors=False, cond=None):
1818
self.func = func
1919
self.suppress_errors = suppress_errors
20+
self.cond = Callbacks(factory=ConditionWrapper).add(cond)
2021
self._callback = None
2122

2223
def __repr__(self):
@@ -42,6 +43,7 @@ def setup(self, resolver):
4243
resolver (callable): A method responsible to build and return a valid callable that
4344
can receive arbitrary parameters like `*args, **kwargs`.
4445
"""
46+
self.cond.setup(resolver)
4547
try:
4648
self._callback = resolver(self.func)
4749
return True
@@ -132,7 +134,14 @@ def clear(self):
132134
self.items = []
133135

134136
def call(self, *args, **kwargs):
135-
return [callback(*args, **kwargs) for callback in self.items]
137+
return [
138+
callback(*args, **kwargs)
139+
for callback in self.items
140+
if callback.cond.all(*args, **kwargs)
141+
]
142+
143+
def all(self, *args, **kwargs):
144+
return all(condition(*args, **kwargs) for condition in self)
136145

137146
def _add(self, func, resolver=None, prepend=False, **kwargs):
138147
if func in self.items:

statemachine/event.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,14 @@ def trigger_event(self, *args, **kwargs):
5858
trigger_event._is_sm_event = True
5959

6060
return trigger_event
61+
62+
63+
def same_event_cond_builder(expected_event: str):
64+
"""
65+
Builds a condition method that evaluates to ``True`` when the expected event is received.
66+
"""
67+
68+
def cond(event: str) -> bool:
69+
return event == expected_event
70+
71+
return cond

statemachine/transition.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from .callbacks import Callbacks
55
from .callbacks import ConditionWrapper
6+
from .event import same_event_cond_builder
67
from .events import Events
78
from .exceptions import InvalidDefinition
89

@@ -96,18 +97,12 @@ def _add_observer(self, *resolvers):
9697
on("on_transition", prepend=True)
9798

9899
for event in self._events:
99-
before(f"before_{event}")
100-
on(f"on_{event}")
101-
after(f"after_{event}")
100+
before(f"before_{event}", cond=same_event_cond_builder(event))
101+
on(f"on_{event}", cond=same_event_cond_builder(event))
102+
after(f"after_{event}", cond=same_event_cond_builder(event))
102103

103104
after("after_transition")
104105

105-
def _eval_cond(self, event_data):
106-
return all(
107-
condition(*event_data.args, **event_data.extended_kwargs)
108-
for condition in self.cond
109-
)
110-
111106
def match(self, event):
112107
return self._events.match(event)
113108

@@ -123,8 +118,9 @@ def add_event(self, value):
123118
self._events.add(value)
124119

125120
def execute(self, event_data: "EventData"):
126-
self.validators.call(*event_data.args, **event_data.extended_kwargs)
127-
if not self._eval_cond(event_data):
121+
args, kwargs = event_data.args, event_data.extended_kwargs
122+
self.validators.call(*args, **kwargs)
123+
if not self.cond.all(*args, **kwargs):
128124
return False
129125

130126
result = event_data.machine._activate(event_data)
Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
"""
2-
Traffic light machine
3-
---------------------
42
5-
Demonstrates the concept of ``cycle`` states.
3+
-------------------
4+
Reusing transitions
5+
-------------------
6+
7+
This example helps to turn visual the different compositions of how to declare
8+
and bind :ref:`transitions` to :ref:`event`.
9+
10+
.. note::
11+
12+
Even sharing the same transition instance, only the transition actions associated with the
13+
event will be called.
14+
15+
16+
TrafficLightMachine
17+
The same transitions are bound to more than one event.
18+
19+
TrafficLightIsolatedTransitions
20+
We define new transitions, thus, isolating the connection
21+
between states.
622
723
"""
824
from statemachine import State
@@ -15,7 +31,64 @@ class TrafficLightMachine(StateMachine):
1531
yellow = State()
1632
red = State()
1733

34+
slowdown = green.to(yellow)
35+
stop = yellow.to(red)
36+
go = red.to(green)
37+
38+
cycle = slowdown | stop | go
39+
40+
def before_slowdown(self):
41+
print("Slowdown")
42+
43+
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
44+
message = ". " + message if message else ""
45+
return f"Running {event} from {source.id} to {target.id}{message}"
46+
47+
def on_enter_red(self):
48+
print("Don't move.")
49+
50+
def on_exit_red(self):
51+
print("Go ahead!")
52+
53+
54+
# %%
55+
# Run a transition
56+
57+
sm = TrafficLightMachine()
58+
sm.send("cycle")
59+
60+
61+
# %%
62+
63+
64+
class TrafficLightIsolatedTransitions(StateMachine):
65+
"A traffic light machine"
66+
green = State(initial=True)
67+
yellow = State()
68+
red = State()
69+
70+
slowdown = green.to(yellow)
71+
stop = yellow.to(red)
72+
go = red.to(green)
73+
1874
cycle = green.to(yellow) | yellow.to(red) | red.to(green)
1975

20-
def on_cycle(self, event_data):
21-
return f"Running {event_data.event} from {event_data.source.id} to {event_data.target.id}"
76+
def before_slowdown(self):
77+
print("Slowdown")
78+
79+
def before_cycle(self, event: str, source: State, target: State, message: str = ""):
80+
message = ". " + message if message else ""
81+
return f"Running {event} from {source.id} to {target.id}{message}"
82+
83+
def on_enter_red(self):
84+
print("Don't move.")
85+
86+
def on_exit_red(self):
87+
print("Go ahead!")
88+
89+
90+
# %%
91+
# Run a transition
92+
93+
sm2 = TrafficLightIsolatedTransitions()
94+
sm2.send("cycle")

tests/test_events.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class TrafficLightMachine(StateMachine):
1313
yellow.to(red, event="cycle stop")
1414
red.to(green, event="cycle go")
1515

16-
def on_cycle(self, event_data, event: str = ""):
16+
def on_cycle(self, event_data, event: str):
17+
assert event_data.event == event
1718
return (
1819
f"Running {event} from {event_data.transition.source.id} to "
1920
f"{event_data.transition.target.id}"

tests/testcases/issue308.md

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
### Issue 308
22

3-
A StateMachine that exercices the example given on issue
3+
A StateMachine that exercises the example given on issue
44
#[308](https://github.com/fgmacedo/python-statemachine/issues/308).
55

6-
On this example, we share the transitions list between events.
6+
In this example, we share the transition list between events.
77

88
```py
99
>>> from statemachine import StateMachine, State
@@ -14,12 +14,12 @@ On this example, we share the transitions list between events.
1414
... state3 = State('s3')
1515
... state4 = State('s4', final=True)
1616
...
17-
... trans12 = state1.to(state2)
18-
... trans23 = state2.to(state3)
19-
... trans34 = state3.to(state4)
17+
... event1 = state1.to(state2)
18+
... event2 = state2.to(state3)
19+
... event3 = state3.to(state4)
2020
...
2121
... # cycle = state1.to(state2) | state2.to(state3) | state3.to(state4)
22-
... cycle = trans12 | trans23 | trans34
22+
... cycle = event1 | event2 | event3
2323
...
2424
... def before_cycle(self):
2525
... print("before cycle")
@@ -55,31 +55,31 @@ On this example, we share the transitions list between events.
5555
... print('exit state4')
5656
...
5757
... def before_trans12(self):
58-
... print('before trans12')
58+
... print('before event1')
5959
...
6060
... def on_trans12(self):
61-
... print('on trans12')
61+
... print('on event1')
6262
...
6363
... def after_trans12(self):
64-
... print('after trans12')
64+
... print('after event1')
6565
...
6666
... def before_trans23(self):
67-
... print('before trans23')
67+
... print('before event2')
6868
...
6969
... def on_trans23(self):
70-
... print('on trans23')
70+
... print('on event2')
7171
...
7272
... def after_trans23(self):
73-
... print('after trans23')
73+
... print('after event2')
7474
...
7575
... def before_trans34(self):
76-
... print('before trans34')
76+
... print('before event3')
7777
...
7878
... def on_trans34(self):
79-
... print('on trans34')
79+
... print('on event3')
8080
...
8181
... def after_trans34(self):
82-
... print('after trans34')
82+
... print('after event3')
8383
...
8484

8585
```
@@ -94,35 +94,26 @@ enter state1
9494
>>> m.state1.is_active, m.state2.is_active, m.state3.is_active, m.state4.is_active, m.current_state ; _ = m.cycle()
9595
(True, False, False, False, State('s1', id='state1', value='state1', initial=True, final=False))
9696
before cycle
97-
before trans12
9897
exit state1
9998
on cycle
100-
on trans12
10199
enter state2
102100
after cycle
103-
after trans12
104101

105102
>>> m.state1.is_active, m.state2.is_active, m.state3.is_active, m.state4.is_active, m.current_state ; _ = m.cycle()
106103
(False, True, False, False, State('s2', id='state2', value='state2', initial=False, final=False))
107104
before cycle
108-
before trans23
109105
exit state2
110106
on cycle
111-
on trans23
112107
enter state3
113108
after cycle
114-
after trans23
115109

116110
>>> m.state1.is_active, m.state2.is_active, m.state3.is_active, m.state4.is_active, m.current_state ; _ = m.cycle()
117111
(False, False, True, False, State('s3', id='state3', value='state3', initial=False, final=False))
118112
before cycle
119-
before trans34
120113
exit state3
121114
on cycle
122-
on trans34
123115
enter state4
124116
after cycle
125-
after trans34
126117

127118
>>> m.state1.is_active, m.state2.is_active, m.state3.is_active, m.state4.is_active, m.current_state
128119
(False, False, False, True, State('s4', id='state4', value='state4', initial=False, final=True))

0 commit comments

Comments
 (0)