Search code examples
pytransitions

Adding and triggering a transition within on_enter state callback


I want to store a previous state A when entering state B using a state history. Later 'on_enter' of another state X, add a transition X->A, trigger it and return to state A.

A -> B (store A) -> C -> X (add transition from X to A) -> A

from transitions.extensions import HierarchicalGraphMachine


class Matter(HierarchicalGraphMachine):
    def __init__(self, states):
        # self.separator = '/'
        self.history = []
        self.stored = []
        HierarchicalGraphMachine.__init__(
            self,
            states=states,
            initial='root',
            show_state_attributes=True
        )

    @property
    def state(self):
        return self.history[-1]

    @state.setter
    def state(self, value):
        self.history.append(value)

    def store_last(self):
        self.stored.append(self.history[-2])

    def to_stored(self):
        stored = self.stored.pop()
        temp_transition = {
            'trigger': 'jump',
            'source': self.state,
            'dest': stored
        }
        self.add_transition(**temp_transition)
        self.trigger('jump')
        self.remove_transition(**temp_transition)


TRANSITIONS = [
  ['walk', 'standing', 'walking'],
  ['stop', 'walking', 'standing'],
  ['drink', '*', 'caffeinated'],
  ['walk',
   ['caffeinated', 'caffeinated_dithering'],
   'caffeinated_running'],
  ['relax', 'caffeinated', 'standing']
]

STATES = [{
    'name': 'root',
    'initial': 'standing',
    'transitions': TRANSITIONS,
    'children': [
        {
            'name': 'standing',
        },
        {
            'name': 'walking',
            'on_enter': 'store_last',
        },
        {
            'name': 'caffeinated',
            'children': [
                'dithering',
                {
                    'name': 'running',
                    'on_enter': 'to_stored'
                }
            ]
        }
    ]
}]

If I run the code skipping on_enter the transition is added, triggered and removed as expected.

# this works
lump = Matter(states=STATES)
lump.trigger('walk')
assert lump.history == ['root', 'root_standing', 'root_walking']
assert lump.stored == ['root_standing']
lump.trigger('drink')
# set state manually to skip on_enter
lump.state = 'root_caffeinated_running'
# run on_enter method manually works
lump.to_stored()
assert lump.state == 'root_standing'
assert lump.stored == []
assert lump.get_nested_transitions(trigger='jump') == []
lump.get_graph().draw('my_state_diagram.pdf', prog='dot')

If I run it within on_enter I get an error "MachineError: "Can't trigger event 'jump' from state(s) root_caffeinated_running!"

# cannot do the jump using on_enter
lump = Matter(states=STATES)
lump.trigger('walk')
lump.trigger('drink')
lump.trigger('walk')
# Can't trigger event 'jump' from state(s) root_caffeinated_running!
lump.get_graph().draw('my_state_diagram.pdf', prog='dot')

Solution

  • By default transitions will add auto transitions which can be used to reach every state directly without the need to add transitions temporarily. In combination with Machine.trigger that can be used to call events by their name, your to_stored method could be simplified to:

        def to_stored(self):
            stored = self.stored.pop()
            self.trigger(f"to_{stored}")