pytransitions / transitions

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

Deeper nested transitions #554

Closed jankrejci closed 2 years ago

jankrejci commented 2 years ago

Hi, I have a problem with nested transition. I would like to use 3 levels of the nested machine. The first machine runs the second machine as children and the second machine runs the third. When the third machine's job is done it jumps to the done state, which is remapped back to the second machine and there is remapped back to the first machine.

Unfortunately, this works only if I'm using queued=False. But I would like to use queued transitions to have shorter stack and tracebacks in case of error. When I try to switch to queued=True then the transitions are stuck at the third state.

I have described the problem in more detail on SO already, but no reaction so far. StackOverflow link

transitions 0.8.10
python 3.7.3

Here is the complete example

import logging as log
from transitions.extensions import HierarchicalMachine

class GenericMachine(HierarchicalMachine):
    def __init__(self, states, transitions, model=None):

        generic_states = [
            {"name": "initial", "on_enter": self.entry_initial},
            {"name": "done", "on_enter": self.entry_done},
        ]
        states += generic_states

        super().__init__(
            states=states,
            transitions=transitions,
            model=model,
            send_event=True,
            queued=True,
        )

    def entry_initial(self, event_data):
        raise NotImplementedError

    def entry_done(self, event_data):
        raise NotImplementedError

class MainMachine(GenericMachine):
    def __init__(self):
        nested = NestedMachine()
        remap = {"done": "done"}
        states = [
            {"name": "nested", "children": nested, "remap": remap},
        ]
        transitions = [
            ["go", "initial", "nested"],
        ]
        super().__init__(states, transitions, model=self)

    def entry_done(self, event_data):
        print("job finished")

class NestedMachine(GenericMachine):
    def __init__(self):
        deeper = DeeperMachine()
        remap = {"done": "done"}
        states = [
            {"name": "deeper", "children": deeper, "remap": remap},
        ]
        transitions = [
            ["go", "initial", "deeper"],
        ]
        super().__init__(states, transitions)

    def entry_initial(self, event_data):
        event_data.model.go()

class DeeperMachine(GenericMachine):
    def __init__(self):
        states = [
            {"name": "working", "on_enter": self.entry_working},
        ]
        transitions = [
            ["go", "initial", "working"],
            ["go", "working", "done"],
        ]
        super().__init__(states, transitions, model=self)

    def entry_initial(self, event_data):
        event_data.model.go()

    def entry_working(self, event_data):
        event_data.model.go()

def main():
    log.basicConfig(level=log.DEBUG)
    log.getLogger("transitions").setLevel(log.INFO)
    machine = MainMachine()
    machine.go()
    assert machine.state == "done"

if __name__ == "__main__":
    main()
aleneum commented 2 years ago

Hello @jankrejci,

just FYI: I will start tackling open Issues here and on SO starting from 15th of November.

jankrejci commented 2 years ago

@aleneum great, thanks for info

aleneum commented 2 years ago

Hello @jankrejci,

I had a look at your issue and can confirm this is a bug. Queuing happens 'too late' and thus recreating the proper context for the execution of the queued trigger always becomes an issue. Especially since you work a lot with references to other models/machines. I decided to refactor the HSMs to queue triggers BEFORE the proper context has been decided and also added your example as a test case. This will be part of the next major release 0.9. If you already want to give it a try, check the dev-0.9 branch of the repository.

jankrejci commented 2 years ago

@aleneum great thanks, I will give a try tomorrow

jankrejci commented 2 years ago

I have tested the examples and also the production code. Everything works well. Thanks

aleneum commented 2 years ago

Nice,

if you stumble upon more issues, let me know. There is still some typing/documentation work to do for 0.9 but I plan to be done with it this year and release transitions 0,9 in December.

jankrejci commented 2 years ago

ok great

I have one more issue on SO, it is related to conditional transitions link

aleneum commented 2 years ago

Give dev-0.9 another try. self wasn't always the same in your callbacks. Or in other words: you set self._error on one instance of NestedMachine and checked it on another. This should be prevented now.

jankrejci commented 2 years ago

It is working as expected now :)

aleneum commented 2 years ago

Btw. It seems you are shaking quite dusty spots of transitions. Feel free to open issues directly (without the SO loop) in case transitions does not behave how you would expect it to behave.

aleneum commented 2 years ago

This bug should be fixed in master now and will be part of the 0.9.0 release. Thanks again for taking the time to document the issue and test my changes. If you still face this issue let me know and I will reopen the issue if necessary.