Skip to content
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

Event "Union" does not execute transition actions #453

Closed
ostetzer opened this issue Jun 26, 2024 · 4 comments
Closed

Event "Union" does not execute transition actions #453

ostetzer opened this issue Jun 26, 2024 · 4 comments

Comments

@ostetzer
Copy link

  • Python State Machine version: 2.3.1
  • Python version: 3.11
  • Operating System: Windows and Raspian

Description

Describe what you were trying to get done.
As in the traffic light example, I combined several transitions int a "cycle" with a union. e.g.
shutdown_cycle = stop | shutdown
My expectation is, that when I call that "union" event, then the appropriate transition (based on curernt state) is executed.

Tell us what happened, what went wrong, and what you expected to happen.
When I tried that, the state change did happen, but the actions of a the transitions are not executed.

In the example, I call the shutdown-cycle twice from state running and i expect the transitions stop and shutdown to be executed.
However, the functions on_stop() and on_shutdown() are not called/executed when i call shutdown_cycle()

What I Did

I created a minimum example to demonstrate my findings. IS my expectations wrong?

Minimum example:

from statemachine import StateMachine, State

class test_FSM(StateMachine):
    init = State(initial=True)
    standby = State()
    running = State()
    off = State(final=True)

    start = init.to(standby)
    run = standby.to(running)
    stop = running.to(standby)
    shutdown = standby.to(off)

    shutdown_cycle = stop | shutdown

    def __init__(self):
        super().__init__()

    def on_start(self):
        print("initialize to standby")

    def on_run(self):
        print("changed to running")

    def on_stop(self):
        print("stopping")

    def on_shutdown(self):
        print("shutting down")


if __name__ == '__main__':
    test_machine = test_FSM()
    test_machine.start()
    test_machine.run()
    print(test_machine.current_state)
    test_machine.shutdown_cycle()
    print(test_machine.current_state)
    test_machine.shutdown_cycle()
 

output:

initialize to standby
changed to running
Running
Standby
Off

@ostetzer
Copy link
Author

I have experimented a bit more. When I define the event like this:

stop = running.to(standby, event="shutdown_cycle")
shutdown = standby.to(off, event="shutdown_cycle")

the function on_stop() or on_shutdown() is also not called, when I call the event like in the example above.

@fgmacedo
Copy link
Owner

fgmacedo commented Jun 26, 2024

Hi @ostetzer , how are you? Thanks for getting in touch.

Sorry about the misleading behavior. This theme is a huge opportunity to improve the docs.

This is indeed the expected behavior... To make it clear, I've created a graph representation of the example state machine you provided:

>>> test_machine._graph().write_png("test_FSM.png")

test_FSM

So, what's going on?

Note that on this graph, you have the transitions represented as the edges (arcs) that connect the states, and the events as labels attached to these edges.

Note that the transition is created using the pattern <origin>.to(<destination>), as in running.to(standby) by example.

When we assign the transition list to a variable at the class level, we're defining an event. So by declaring stop = running.to(standby), we're defining an event called stop, that will be bound to the transition running.to(standby).

And by declaring shutdown_cycle = stop | shutdown, we're assigning the two list of transitions at stop and shutdown to another event called shutdown_cycle.

Given that you're binding actions by naming convention, by definition:

The action will be registered for every Transition associated with the event.

The key here is that using this naming convention pattern, the action is associated with the event name, not with the transition. The action name matters and must match the pattern of the event.

Why the actions on_stop or on_shutdown are not performed when I trigger the event shutdown_cycle:
Answer: Because these actions are related to the events stop or shutdown, not to the event shutdown_cycle.

How to accomplish the expected behaviour

You can explicitly bind the action to the transition itself, using params, like running.to(standby, on="on_stop"). Note that now the name of the event does not matter, the only requirement is to have a method, attribute or property with the name specified.

class test_FSM(StateMachine):
    init = State(initial=True)
    standby = State()
    running = State()
    off = State(final=True)

    start = init.to(standby)
    run = standby.to(running)
    stop = running.to(standby, on="on_stop")
    shutdown = standby.to(off, on="on_shutdown")

    shutdown_cycle = stop | shutdown

    def __init__(self):
        super().__init__()

    def on_start(self):
        print("initialize to standby")

    def on_run(self):
        print("changed to running")

    def on_stop(self):
        print("stopping")

    def on_shutdown(self):
        print("shutting down")

So if you go with this alternative, I suggest changing the action name to another thing not similar to the naming convention, just to make explicitly that you're binding an action to the transition itself.

Like this:

class test_FSM(StateMachine):
    init = State(initial=True)
    standby = State()
    running = State()
    off = State(final=True)

    start = init.to(standby)
    run = standby.to(running)
    stop = running.to(standby, on="_on_stop")
    shutdown = standby.to(off, on="_on_shutdown")

    shutdown_cycle = stop | shutdown

    def __init__(self):
        super().__init__()

    def on_start(self):
        print("initialize to standby")

    def on_run(self):
        print("changed to running")

    def _on_stop(self):
        print("stopping")

    def _on_shutdown(self):
        print("shutting down")
        

The last possibility is to explicitly bind the action to a transitions list using decorators, again, with this binding the name of the action method does not matter.

class test_FSM(StateMachine):
    init = State(initial=True)
    standby = State()
    running = State()
    off = State(final=True)

    start = init.to(standby)
    run = standby.to(running)
    stop = running.to(standby)
    shutdown = standby.to(off)

    shutdown_cycle = stop | shutdown

    def __init__(self):
        super().__init__()

    def on_start(self):
        print("initialize to standby")

    def on_run(self):
        print("changed to running")

    @stop.on
    def _on_stop(self):
        print("stopping")

    @shutdown.on
    def _on_shutdown(self):
        print("shutting down")

Extra

Just to make a point on how things work internally, the <event_name> = <source_state>.to(<target_state>) is only syntatic sugar for <source_state>.to(<target_state>, event="<event_name>).

Example that also works as you expect:

class test_FSM(StateMachine):
    init = State(initial=True)
    standby = State()
    running = State()
    off = State(final=True)

    init.to(standby, event="start")
    standby.to(running, event="run")
    running.to(standby, event=["stop", "shutdown_cycle"], on="_on_stop")
    standby.to(off, event=["shutdown", "shutdown_cycle"], on="_on_shutdown")

    def __init__(self):
        super().__init__()

    def on_start(self):
        print("initialize to standby")

    def on_run(self):
        print("changed to running")

    def _on_stop(self):
        print("stopping")

    def _on_shutdown(self):
        print("shutting down")

What do you think about this alternative syntax? :)

Please let me know if I have clarified the behavior, or if you have any other questions.

@ostetzer
Copy link
Author

Thank you so much for this detailed answer with even different options. Yes, please add this to the documentation asI hope it might help others too. I like the decorator version and will implement this. Thanks a lot! :-)

@fgmacedo
Copy link
Owner

You're very welcome! I'm glad the options were helpful. I'll definitely document this for others too. Best!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants