Gulzar
Gulzar

Reputation: 27946

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.

Upvotes: 1

Views: 130

Answers (2)

aleneum
aleneum

Reputation: 2273

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):

Upvotes: 1

Gulzar
Gulzar

Reputation: 27946

What I ended up doing was after the machine was already instantiated with its transitions:

self.machine.get_state(states.crystal.name).ignore_invalid_triggers = True

I really don't like that solution, as it defeats the very clean design of transitions initialization as a list, but it is all I could find and it does work.

Upvotes: 0

Related Questions