pytransitions / transitions

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

Reflexive + global transitions with hierarchical machines don't return to specific state #568

Closed jnu closed 1 year ago

jnu commented 2 years ago

Thank you for taking the time to report a bug! Your support is essential for the maintenance of this project. Please fill out the following fields to ease bug hunting and resolving this issue as soon as possible:

Describe the bug A reflexive transition = with global starting state * initiated within a child state in a hierarchical machine does not return to that initial child state.

Minimal working example

states = ['a', {'name': 'sub', 'children':['b']}]
transitions = [
  ['reflex', '*', '='],
]

machine = HierarchicalMachine(states=states, transitions=transitions, initial='a')

machine.set_state('sub_b')

print(machine.state)
# -> 'sub_b'

machine.reflex()
# -> True, indicating the transition was successful

print(machine.state)
# -> 'sub'

Expected behavior I would expect the final state in the example to be the starting state, sub_b.

Additional context The problem does not happen without the global *. If I specify the starting states explicitly, including the specific child state sub_b, this works as expected:

# These all correctly return to `sub_b`:
transitions = [
  ['good_reflex_1', 'sub_b', '='],
  ['good_reflex_2', ['sub', 'sub_b'], '='],
  ['good_reflex_3', ['a', 'sub', 'sub_b'], '='],
]

Instead, the behavior of * seems equivalent to only specifying the name of the child machine, without any specific states:

# These all function the same, returning to `sub` instead of `sub_b`:
transitions = [
  ['bad_reflex_1', 'sub', '='],
  ['bad_reflex_2', ['a', 'sub'], '='],
  ['bad_reflex_3', '*', '='],
]
aleneum commented 2 years ago

Hello @jnu,

I can confirm this. This case needs to be handled specifically. I started working on a fix for this.

lostcontrol commented 2 years ago

Hi. I'm not sure this is the same bug but here we go. Upgrading from 0.8.9 to 0.8.10, some of our unit tests started to fail. I can reproduce the issue with the following code:

from transitions.extensions import HierarchicalMachine

class Bug:

    states = [
        "stopped",
        "idle",
        {
            "name": "image",
            "children": [
                "waiting",
                "take",
                "results",
            ]
        },
    ]

    def __init__(self) -> None:

        # Initialize the state machine
        self.__machine = HierarchicalMachine(model=self, states=self.states, initial="stopped")

        self.__machine.add_transition(
            "trigger_image",
            "stopped",
            "image_take"
        )

        self.__machine.add_transition(
            "trigger_image",
            "image",
            "image_take"
        )

    def on_enter_image(self):
        print("Entering state image")

    def on_enter_image_take(self):
        print("Entering state image_take")

b = Bug()
print(b.state)

b.trigger_image()
print(b.state)

b.trigger_image()
print(b.state)

Here is the output with 0.8.10 and 0.8.11:

stopped
Entering state image
Entering state image_take
image_take
image_take

Here is what we get with 0.8.9 and what is expected:

stopped
Entering state image
Entering state image_take
image_take
Entering state image_take
image_take
aleneum commented 2 years ago

that's probably a different issue. thank you for providing an MRE. I'll let you know when I fixed it.

lostcontrol commented 2 years ago

that's probably a different issue. thank you for providing an MRE. I'll let you know when I fixed it.

Thank you. Do you want me to open a new issue?

aleneum commented 2 years ago

nah, one issue is fine, thanks for asking.