pytransitions / transitions

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

Inconsistant handling of on_enter_*/on_exit_* methods in LockedHierarchicalMachine for nested states since version 0.8.0 #477

Closed oliver-goetz closed 3 years ago

oliver-goetz commented 3 years ago

When using a joined Model/Machine class together with a LockedHierarchicalMachine it handles custom onenter/onexit for nested states different than a HierarchicalMachine since version 0.8.0.

I setup a small working example using a template from README.md

from transitions.extensions import HierarchicalMachine as Machine

states = ['standing', 'walking', {'name': 'caffeinated', 'children': ['dithering', 'running']}]
transitions = [
    ['walk', 'standing', 'walking'],
    ['stop', 'walking', 'standing'],
    ['drink', '*', 'caffeinated'],
    ['walk', ['caffeinated', 'caffeinated_dithering'], 'caffeinated_running'],
    ['relax', 'caffeinated', 'standing']
]

class MTest(Machine):
    def on_enter_walking(self):
        print('CUSTOM on_enter_walking')

    def on_exit_walking(self):
        print('CUSTOM on_exit_walking')

    def on_enter_caffeinated_running(self):
        print('CUSTOM on_enter_caffeinated_running')

    def on_exit_caffeinated_running(self):
        print('CUSTOM on_exit_caffeinated_running')

machine = MTest(states=states, transitions=transitions, initial='standing', ignore_invalid_triggers=True)

machine.walk()  # Walking now
machine.stop()  # let's stop for a moment
machine.drink()  # coffee time
machine.state
machine.walk()  # we have to go faster
machine.state
machine.stop()  # can't stop moving!
machine.state
machine.relax()  # leave nested state
machine.state  # phew, what a ride

The output includes the printed lines of all 4 custom onenter and onexit methods.

CUSTOM on_enter_walking
CUSTOM on_exit_walking
CUSTOM on_enter_caffeinated_running
CUSTOM on_exit_caffeinated_running

Now i justed change the import line to from transitions.extensions import LockedHierarchicalMachine as Machine and the whole program looks like this

from transitions.extensions import LockedHierarchicalMachine as Machine

states = ['standing', 'walking', {'name': 'caffeinated', 'children': ['dithering', 'running']}]
transitions = [
    ['walk', 'standing', 'walking'],
    ['stop', 'walking', 'standing'],
    ['drink', '*', 'caffeinated'],
    ['walk', ['caffeinated', 'caffeinated_dithering'], 'caffeinated_running'],
    ['relax', 'caffeinated', 'standing']
]

class MTest(Machine):
    def on_enter_walking(self):
        print('CUSTOM on_enter_walking')

    def on_exit_walking(self):
        print('CUSTOM on_exit_walking')

    def on_enter_caffeinated_running(self):
        print('CUSTOM on_enter_caffeinated_running')

    def on_exit_caffeinated_running(self):
        print('CUSTOM on_exit_caffeinated_running')

machine = MTest(states=states, transitions=transitions, initial='standing', ignore_invalid_triggers=True)

machine.walk()  # Walking now
machine.stop()  # let's stop for a moment
machine.drink()  # coffee time
machine.state
machine.walk()  # we have to go faster
machine.state
machine.stop()  # can't stop moving!
machine.state
machine.relax()  # leave nested state
machine.state  # phew, what a ride

The new output looks like this.

CUSTOM on_enter_walking
CUSTOM on_exit_walking

The custom on_enter/on_exit methods of caffeinated_running state are not executed anymore. Analyzing the class shows that there are methods for on_enter_caffeinated_running and on_exit_caffeinated_running. But those are the generated functions you can use to add custom callbacks.

I would expect that custom onenter and onexit methods of a LockedHierarchicalMachine are handled the same way than those of the HierarchicalMachine as it was up to version 0.7.2.

aleneum commented 3 years ago

Hello @oliver-goetz,

thank you for the report. This is the nested version of #214 which hasnt been detected since we never tested nested model callbacks where the machine acts as a model. The bug and the lack of testing should be fixed now.

aleneum commented 3 years ago

This should be fixed with 0.8.4. Feel free to comment if you face more issues using nested states and model callbacks. I will reopen the issue if the bug still persists

oliver-goetz commented 3 years ago

This is working too, thanks 👍