-
Notifications
You must be signed in to change notification settings - Fork 82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Previous transition before/on/after methods are called again, sometimes #308
Comments
Hi @Kevin-Prichard , thanks for getting in touch. This is the resulting SM from your example: So what's happening here? The example binds the same set of transitions to multiple events, and also merges the list of transitions. These lines: trans12 = state1.to(state2) # this returns a `TransitionList` that is bound to the name `trans12`
trans23 = state2.to(state3) # this returns a `TransitionList` that is bound to the name `trans23`
trans34 = state3.to(state4) # this returns a `TransitionList` that is bound to the name `trans23`
# `TransitionList` implements the `|` or operator, and the boolean expression is evaluated from left to right,
# Here I've found a bug, thanks! The `|` instead of returning a new object, was merging the right operand into itself.
cycle = trans12 | trans23 | trans34
So instead of two transitions triggered by distinct events, you got the same transition triggered by multiple events. Bug found! The And this is what happened when I fixed the
It's the equivalent of: class TestSM(StateMachine):
state1 = State('state1', initial=True)
state2 = State('state2')
state3 = State('state3')
state4 = State('state4', final=True)
state1.to(state2, event="trans12 cycle")
state2.to(state3, event="trans23 cycle")
state3.to(state4, event="trans34 cycle") And indeed is your expected behaviur if I got it right. Alternative~We cannot reuse the same transition definitions across events to get the desired behavior. ~ So instead of reusing the previous from statemachine import StateMachine, State
class TestSM(StateMachine):
state1 = State('state1', initial=True)
state2 = State('state2')
state3 = State('state3')
state4 = State('state4', final=True)
trans12 = state1.to(state2)
trans23 = state2.to(state3)
trans34 = state3.to(state4)
cycle = state1.to(state2) | state2.to(state3) | state3.to(state4) This is the SM Thanks for giving StateMachine a try. Let me know if you have any other issues. I'm planning to release the This is the Feel free to reopen or reply to this or another issue. Best regards, EDIT: Updated now that I think that I understood your need. |
A file that reproduces the fix: Issue 308A StateMachine that exercices the example given on issue >>> from statemachine import StateMachine, State
>>> class TestSM(StateMachine):
... state1 = State('s1', initial=True)
... state2 = State('s2')
... state3 = State('s3')
... state4 = State('s4', final=True)
...
... trans12 = state1.to(state2)
... trans23 = state2.to(state3)
... trans34 = state3.to(state4)
...
... cycle = state1.to(state2) | state2.to(state3) | state3.to(state4)
... # cycle = trans12 | trans23 | trans34
...
... def before_cycle(self):
... print("before cycle")
...
... def on_cycle(self):
... print("on cycle")
...
... def after_cycle(self):
... print("after cycle")
...
... def on_enter_state1(self):
... print('enter state1')
...
... def on_exit_state1(self):
... print('exit state1')
...
... def on_enter_state2(self):
... print('enter state2')
...
... def on_exit_state2(self):
... print('exit state2')
...
... def on_enter_state3(self):
... print('enter state3')
...
... def on_exit_state3(self):
... print('exit state3')
...
... def on_enter_state4(self):
... print('enter state4')
...
... def on_exit_state4(self):
... print('exit state4')
...
... def before_trans12(self):
... print('before trans12')
...
... def on_trans12(self):
... print('on trans12')
...
... def after_trans12(self):
... print('after trans12')
...
... def before_trans23(self):
... print('before trans23')
...
... def on_trans23(self):
... print('on trans23')
...
... def after_trans23(self):
... print('after trans23')
...
... def before_trans34(self):
... print('before trans34')
...
... def on_trans34(self):
... print('on trans34')
...
... def after_trans34(self):
... print('after trans34')
...
Example given:
```py
>>> m = TestSM()
enter state1
>>> m._graph().write_png("issue308_fix_transition_list.png")
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(True, False, False, False, State('s1', id='state1', value='state1', initial=True, final=False))
before cycle
exit state1
on cycle
enter state2
after cycle
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(False, True, False, False, State('s2', id='state2', value='state2', initial=False, final=False))
before cycle
exit state2
on cycle
enter state3
after cycle
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(False, False, True, False, State('s3', id='state3', value='state3', initial=False, final=False))
before cycle
exit state3
on cycle
enter state4
after cycle
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state
(False, False, False, True, State('s4', id='state4', value='state4', initial=False, final=True)) |
All understood, thanks for explaining, @fgmacedo . To clarify, the way I did it generated a I understand now that the way I defined But, with There's only Unless I'm missing something? In your last example, we see the exit and entry of states. If I have to inspect To review, what I thought I saw, in the README and docs, was that custom methods per transition were allowed. After reading through everything, it looks like it's only allowed in two cases, 1) using the decorators on by Take the case of bank transactions. I want to implement behavior for each state transition. I don't want to re-execution a prior transition! That would mess with state. Maybe I have missed something. |
Hi @Kevin-Prichard , thanks for your time reviewing this. Can you please elaborate an example like a test case with assertion to your expected behavior? Trying to figure out generically with pseudo states, transitions and events is harder than with a concrete example. The way you explained if I understood right is how it worked after the fix on the
This was in fact a bug and the fix is already on the But trying to answer your question, it's possible to bind actions to events and for specific transitions. Using a visual graph representation, a transition is the edge, the link between two states. An event is the thing that fired this transition, we use to write the name of the event on the edge. The most common usage is like you said, bind actions by name convention on the event. This is due the fact that So, you can bind actions on the transition by using constructor params. Each transition can receive any number of callbacks:
These names will be searched on the StateMachine and on a model, if one is given. The Let me know if this helps. Best, |
@Kevin-Prichard , now I think that I understood your expected behavior, and indeed the bug fixed at #310 resolves the issue. Issue 308A StateMachine that exercices the example given on issue #308. Now with the fix applied on #310. >>> from statemachine import StateMachine, State
>>> class TestSM(StateMachine):
... state1 = State('s1', initial=True)
... state2 = State('s2')
... state3 = State('s3')
... state4 = State('s4', final=True)
...
... trans12 = state1.to(state2)
... trans23 = state2.to(state3)
... trans34 = state3.to(state4)
...
... cycle = trans12 | trans23 | trans34
...
... def before_cycle(self):
... print("before cycle")
...
... def on_cycle(self):
... print("on cycle")
...
... def after_cycle(self):
... print("after cycle")
...
... def on_enter_state1(self):
... print('enter state1')
...
... def on_exit_state1(self):
... print('exit state1')
...
... def on_enter_state2(self):
... print('enter state2')
...
... def on_exit_state2(self):
... print('exit state2')
...
... def on_enter_state3(self):
... print('enter state3')
...
... def on_exit_state3(self):
... print('exit state3')
...
... def on_enter_state4(self):
... print('enter state4')
...
... def on_exit_state4(self):
... print('exit state4')
...
... def before_trans12(self):
... print('before trans12')
...
... def on_trans12(self):
... print('on trans12')
...
... def after_trans12(self):
... print('after trans12')
...
... def before_trans23(self):
... print('before trans23')
...
... def on_trans23(self):
... print('on trans23')
...
... def after_trans23(self):
... print('after trans23')
...
... def before_trans34(self):
... print('before trans34')
...
... def on_trans34(self):
... print('on trans34')
...
... def after_trans34(self):
... print('after trans34')
... Example given: >>> m = TestSM()
enter state1
>>> m._graph().write_png("issue308_fix_transition_list.png")
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(True, False, False, False, State('s1', id='state1', value='state1', initial=True, final=False))
before cycle
before trans12
exit state1
on cycle
on trans12
enter state2
after cycle
after trans12
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(False, True, False, False, State('s2', id='state2', value='state2', initial=False, final=False))
before cycle
before trans23
exit state2
on cycle
on trans23
enter state3
after cycle
after trans23
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state ; _ = m.cycle()
(False, False, True, False, State('s3', id='state3', value='state3', initial=False, final=False))
before cycle
before trans34
exit state3
on cycle
on trans34
enter state4
after cycle
after trans34
>>> m.is_state1, m.is_state2, m.is_state3, m.is_state4, m.current_state
(False, False, False, True, State('s4', id='state4', value='state4', initial=False, final=True)) You can test this example locally placing the issue308.md file at Best regards, |
I've included a folder |
That's it! It works as I was expecting now, in my local test cases. (I hope this change harmonizes with other developers using of this module in their projects.) And the test added in #310 covers the use-case I was trying to explain. Beautiful. Thank you. |
Nice! I'll close the issue for now. Feel free to reach out. |
@Kevin-Prichard In the upcoming release, I've added a condition on the callback to only run if the callback was associated with the expected event. So even sharing the same transition instance, the action will be only fired if the event name matches. This is a kinda breaking change / fix for your use case: #365 |
Description
While exploring this really promising module, I coded up a state machine to put it through some paces. It has states (state1, state2, state3, state4), transitions (trans12, trans23, trans34), and cycle defined (trans12 | trans23 | trans34).
I noticed what might be either an incorrect assumption on my part, or possibly a bug: the first transition methods (before_/on_/after_ methods) were getting called after that transition has already passed, when later transitions' methods were also being called.
Demo code-
Paste the command(s) you ran and the output.
As mentioned, before_trans12, on_trans12 and after_trans12 continue firing after their turn is finished. Did I misconfigure something in my StateMachine subclass?
I spent some time debug-stepping through metaclass code while importing TestSM. Nothing conclusive, except that the Transition instances, after the first one, always start out containing
"cycle trans12"
before the current transition key is added to self._events.For example, repr of trans23 after initialization, see how
event='cycle trans12 trans23'
? That don't seem right.I'll wait for a reply before continuing diagnosis.
-Kevin
The text was updated successfully, but these errors were encountered: