pgularski / pysm

Versatile and flexible Python State Machine library
http://pysm.readthedocs.io/
MIT License
73 stars 11 forks source link

Accessing self.sm.leaf_state from a event-handler returns wrong state #7

Closed RaphaelMaschinsen closed 5 years ago

RaphaelMaschinsen commented 5 years ago

Hi

I came across this problem and it is very likely that i'm doing something wrong, however i couldn't figure out what it might be.

After i create an object that has a statemachine with entry-handlers for the states, when i call self.sm.leaf_state from within an entry-handler, it will always return the same state, even if the statemachine changes its state.

It's weird because if i access self.sm.leaf_state from outside the entry-handler, for example inside a test() function, it will return the correct states.

This script demonstrates my problem:

from pysm import StateMachine, State, Event

class TestSM():
    def __init__(self):
        self.sm = self._get_state_machine()

    def _get_state_machine(self):
        state_machine = StateMachine('Test')
        state_1 = State('One')
        state_2 = State('Two')

        state_machine.add_state(state_1, initial=True)
        state_machine.add_state(state_2)
        state_machine.add_transition(state_1, state_2, events=['change_state'])
        state_machine.add_transition(state_2, state_1, events=['change_state'])
        state_1.handlers = {'enter': self.entry_func}
        state_2.handlers = {'enter': self.entry_func}

        state_machine.initialize()
        return state_machine

    def entry_func(self, state, event):
        print('Entered State: ' + str(state.name))
        print(self.sm.leaf_state)

    def test(self):
        self.sm.dispatch(Event('change_state'))
        print(self.sm.leaf_state)
        self.sm.dispatch(Event('change_state'))
        print(self.sm.leaf_state)
        self.sm.dispatch(Event('change_state'))
        print(self.sm.leaf_state)

TEST_SM = TestSM()
TEST_SM.test()

The output i get (python 3.5.2):

Entered State: Two
<State One (0x10a4253c8)>
<State Two (0x10a425400)>
Entered State: One
<State One (0x10a4253c8)>
<State One (0x10a4253c8)>
Entered State: Two
<State One (0x10a4253c8)>
<State Two (0x10a425400)>

I would expect for both of the print(self.sm.leaf_state) to be the same. Am i doing something wrong?

Btw thanks again for the great library, i'm using it quite a lot on an ESP32.

pgularski commented 5 years ago

Hi Raphael,

Thanks for submitting this issue along with a great explanation and the code explaining the problem - in fact, I tweaked the code a bit and commited it in as one of the tests.

Initially I wasn't convinced it was a bug as I thought that a leaf_state should be used only from outside the machine state action. But then, as a second thought, I figured out some use cases where it might be useful to actually check the leaf_state from within a transition action (as you did).

So I fixed the issue and now the leaf_state is updated on every enter and exit action so that is always up to date. So far it was dynamically calculated and was returning a valid state only after a transition has settled.

The new approach has its implications though, as now it's illegal to set manually the state machine states once a machine got initialized. I mean it breaks (and actually always did) the machine state integrity. Therefore, the following code is no longer valid and is removed from the tests:

    # Brute force rollback of the previous state
    s3.state = s3.state_stack.pop()
    sm.state = sm.state_stack.pop()
    assert sm.state == s1
    assert sm.leaf_state == s1
    assert s3.state == s31
    assert s3.leaf_state == s31
    assert list(sm.state_stack.deque) == [s1, s2]
    assert list(s3.state_stack.deque) == []

Thanks again that you keep using the library and send me the issues you find. The feedback I got from you helps me a lot!

I released the fixed version as 0.3.9 - it's deployed to pypi.

Piotr