Search code examples
pythontransitionstate-machinepytransitions

Terminal/sink state in pytransitions


I am using pytransitions with a state machine for example

from transitions import Machine
from transitions import EventData


class Matter(object):
    def __init__(self):
        transitions = [
            {'trigger': 'heat', 'source': 'solid', 'dest': 'liquid'},
            {'trigger': 'heat', 'source': 'liquid', 'dest': 'gas'},
            {'trigger': 'cool', 'source': 'gas', 'dest': 'liquid'},
            {'trigger': 'cool', 'source': 'liquid', 'dest': 'solid'}
        ]
        self.machine = Machine(
                model=self,
                states=['solid', 'liquid', 'gas'],
                transitions=transitions,
                initial='solid',
                send_event=True
        )

    def on_enter_gas(self, event: EventData):
        print(f"entering gas from {event.transition.source}")

    def on_enter_liquid(self, event: EventData):
        print(f"entering liquid from {event.transition.source}")

    def on_enter_solid(self, event: EventData):
        print(f"entering solid from {event.transition.source}")

I would like to add a state, for which any trigger remains in the same state, without invoking a transition, and without explicitly specifying every possible trigger, and also without ignoring all invalid triggers (as this is very good for debugging).

I would like for example a state crystal which can be reached by triggering crystalize from liquid, for which any event will do nothing.

Can this be achieved with the library?

Another way to phrase this question would be some way to ignore_invalid_triggers=True only for a specific state, not all states.


Solution

  • Similarly to transitions, states can also be defined with dictionaries:

    from transitions import Machine, MachineError
    
    
    class Matter(object):
        def __init__(self):
            transitions = [
                {'trigger': 'heat', 'source': 'solid', 'dest': 'liquid'},
                {'trigger': 'heat', 'source': 'liquid', 'dest': 'gas'},
                {'trigger': 'cool', 'source': 'gas', 'dest': 'liquid'},
                {'trigger': 'cool', 'source': 'liquid', 'dest': 'solid'},
                # add a transition to 'crystal' which is valid from anywhere
                {'trigger': 'crystallize', 'source': '*', 'dest': 'crystal'},
            ]
            self.machine = Machine(
                    model=self,
                    states=['solid', 'liquid', 'gas',
                            # initialized 'crystal' with dictionary
                            {'name': 'crystal', 'ignore_invalid_triggers': True}],
                    transitions=transitions,
                    initial='solid',
                    send_event=True
            )
    
    
    m = Matter()
    assert m.is_solid()
    try:
        m.cool()  # raises a machine error since cool cannot be called from 'solid'
        assert False
    except MachineError:
        pass
    assert m.crystallize()  # transitions to 'crystal'
    assert m.is_crystal()
    assert not m.heat()  # note that the transition will return 'False' since it did not happen but no exception was thrown
    assert m.is_crystal()  # state machine is still in state 'crystal'
    

    Instead of {'name': 'crystal', 'ignore_invalid_triggers': True} you could also pass State(name='crystal', ignore_invalid_triggers=True) instead. This form is mentioned in the documentation's state section:

    But in some cases, you might want to silently ignore invalid triggers. You can do this by setting ignore_invalid_triggers=True (either on a state-by-state basis, or globally for all states):