pytransitions / transitions

A lightweight, object-oriented finite state machine implementation in Python with many extensions
MIT License
5.68k stars 530 forks source link

On entry/start/load callback for ChildFSM #501

Closed S1MP50N closed 2 years ago

S1MP50N commented 3 years ago

Hi,

I am using nested FSMs, and I cannot find this documented, so raising as a potential feature request and/or looking for guidance on the best way to add this.

Take the following image as a simple example: image

The code to create this looks like this:

from transitions.extensions import HierarchicalMachine
from transitions_gui import NestedWebMachine 

class ChildFSM(HierarchicalMachine):

    def doStuff(self):
        print("doStuff")

    def doOtherStuff(self):
        print("doOtherStuff")

    def __init__(self):
        states = ['doStuff', 'doOtherStuff', 'done']
        HierarchicalMachine.__init__(self, states=states, initial='doStuff', auto_transitions=False, ignore_invalid_triggers=True)
        self.add_transition('change', 'doStuff', 'doOtherStuff', after=self.doOtherStuff)
        self.add_transition('change', 'doOtherStuff', 'doStuff', after=self.doStuff)
        self.add_transition('finish', 'doStuff', 'done')
        self.add_transition('finish', 'doOtherStuff', 'done')

class ParentFSM(NestedWebMachine):

    def __init__(self, _childFSM):
        states = ['init', 'idle',  {'name':'running', 'children':_childFSM }]
        NestedWebMachine.__init__(self, states=states, name = "Test", initial='init', auto_transitions=False, ignore_invalid_triggers=True)
        self.add_transition('wake', 'init', 'idle')
        self.add_transition('start', 'idle', 'running')
        self.add_transition('stop', 'running', 'idle')

childFSM = ChildFSM()
parentFSM = ParentFSM(childFSM)
parentFSM.wake()
parentFSM.start()

try:
    while True:
        time.sleep(5)
except KeyboardInterrupt:  # Ctrl + C will shutdown the machine
    parentFSM.stop_server()

I would like to be able to define a callback in the child FSM that is called when it is started by a parent FSM instantiating it. Something like this (note addition of onEntry on machine init and user onEntry function):

class ChildFSM(HierarchicalMachine):

    def onEntry(self):
        print("initialising variables")

    def doStuff(self):
        print("doStuff using variables")

    def doOtherStuff(self):
        print("doOtherStuff using other variables")

    def __init__(self):
        states = ['doStuff', 'doOtherStuff', 'done']
        HierarchicalMachine.__init__(self, states=states, initial='doStuff', onEntry=self.entry, auto_transitions=False, ignore_invalid_triggers=True)
        self.add_transition('change', 'doStuff', 'doOtherStuff', after=self.doOtherStuff)
        self.add_transition('change', 'doOtherStuff', 'doStuff', after=self.doStuff)
        self.add_transition('finish', 'doStuff', 'done')
        self.add_transition('finish', 'doOtherStuff', 'done')

I appreciate I could solve this by adding an additional 'initial' state to the child FSM with a transition to 'doStuff' and adding an after callback. However, that would then put a dependency on the parent FSM to raise an additional child FSM specific event to cause the initial transition within the child. I don't think that feels very clean? Do you think it would be simple enough to add these callbacks?

Any help / advice appreciated.

Thanks, Shaun

aleneum commented 3 years ago

Hello @S1MP50N,

I appreciate I could solve this by adding an additional 'initial' state to the child FSM with a transition to 'doStuff' and adding an after callback. However, that would then put a dependency on the parent FSM to raise an additional child FSM specific event to cause the initial transition within the child.

When doStuff is your initial state, you can pass 'on_enter' callbacks that will only be called when the state is actually entered. This happens when a) ChildFSM transitions to doStuff or b) ParentFSM enters a state defined in ChildFSM. It will NOT be executed when ChildFSM is initialized since initial states are not entered. The model rather 'spawns' in it. The only thing you have to change is your state initialisation in ChildFSM:

#  ...
states = [{'name': 'doStuff', 'on_enter': self.onEntry}, 'doOtherStuff', 'done']
#  ...

EDIT: Hmm... I checked the README and see that the ability to define states as dictionaries is not covered by the documentation. I will add that shortly.

thedrow commented 3 years ago

Nested state machines are also not documented as far as I can see.

aleneum commented 3 years ago

Are you referring to a particular feature? Because HSMs in general are covered here.

thedrow commented 3 years ago

Yes, using a state machine as children for a specific state.

aleneum commented 3 years ago

There is a section called Reuse of previously created HSMs. Considering your raised issue, I assume your feedback here is that it should be more precise about what parameters accept what kind of input, isn't it?

thedrow commented 3 years ago

Yes, we need to adjust the docstrings.

S1MP50N commented 3 years ago

@aleneum Thanks for the quick response on this. Just managed to test it out and can verify I can use this functionality for child FSM entry transitions in the way I need. It wasn't apparent from the documentation that on_entry fired for child FSM when the parent navigated to them. Anyway, happy to close this, though @thedrow has raised some additional points, so I don't know if you want to address them before we resolve this?

aleneum commented 2 years ago

I pushed 0.9.0 to the master branch which contains pyi stub files that should make it easier to determine the right types for input parameters in the future. If there still are some inconsistencies considering typing let me know in a new issue.