Search code examples
pythonstate-machinepytransitions

How to define enum of Trigger for `transitions` state machine?


As an unrelated followup to this answer, which isuses the following working code:

from transitions import Machine
from transitions import EventData
from typing import Callable
from enum import Enum, auto


class Observer:

    def state_changed(self, event_data: EventData):
        print(f"state is now '{event_data.state.name}'")


class State(Enum):
    SOLID = auto()
    LIQUID = auto()
    GAS = auto()


class SubscribableMachine(Machine):

    transitions = [
        {'trigger': 'heat', 'source': State.SOLID, 'dest': State.LIQUID},
        {'trigger': 'heat', 'source': State.LIQUID, 'dest': State.GAS},
        {'trigger': 'cool', 'source': State.GAS, 'dest': State.LIQUID},
        {'trigger': 'cool', 'source': State.LIQUID, 'dest': State.SOLID}
    ]

    def __init__(self):
        super().__init__(states=State, transitions=self.transitions,
                         initial=State.SOLID, send_event=True)

    def subscribe(self, func: Callable, state: State):
        self.get_state(state).on_enter.append(func)

    def unsubscribe(self, func: Callable, state: State):
        self.get_state(state).on_enter.remove(func)


machine = SubscribableMachine()
observer = Observer()
machine.subscribe(observer.state_changed, State.LIQUID)
machine.heat()  # >>> state is now 'LIQUID'
machine.heat()
assert machine.state == State.GAS
machine.unsubscribe(observer.state_changed, State.LIQUID)
machine.cool()  # no output
assert machine.state == State.LIQUID

I would like to have an enum for Trigger as well, just like I have one for State.

Alas, when I try

class Trigger(Enum):
    heat = auto()
    cool = auto()

and

transitions = [
    {'trigger': Trigger.heat, 'source': State.SOLID, 'dest': State.LIQUID},
    {'trigger': Trigger.heat, 'source': State.LIQUID, 'dest': State.GAS},
    {'trigger': Trigger.cool, 'source': State.GAS, 'dest': State.LIQUID},
    {'trigger': Trigger.cool, 'source': State.LIQUID, 'dest': State.SOLID}
]

def __init__(self):
    super().__init__(states=State, transitions=self.transitions,
                     initial=State.SOLID, send_event=True)

I get

Traceback (most recent call last):
  File "C:/code/EPMD/Kodex/Algorithms/src/python/epmd/ablation_points/queries/dfgfdsg.py", line 42, in <module>
    machine = SubscribableMachine()
  File "C:/code/EPMD/Kodex/Algorithms/src/python/epmd/ablation_points/queries/dfgfdsg.py", line 33, in __init__
    initial=State.SOLID, send_event=True)
  File "C:\Code\EPMD\Kodex\EPD_Prerequisite\python_3.7.6\Lib\site-packages\transitions\core.py", line 589, in __init__
    self.add_model(model)
  File "C:\Code\EPMD\Kodex\EPD_Prerequisite\python_3.7.6\Lib\site-packages\transitions\core.py", line 607, in add_model
    self._add_trigger_to_model(trigger, mod)
  File "C:\Code\EPMD\Kodex\EPD_Prerequisite\python_3.7.6\Lib\site-packages\transitions\core.py", line 813, in _add_trigger_to_model
    self._checked_assignment(model, trigger, partial(self.events[trigger].trigger, model))
  File "C:\Code\EPMD\Kodex\EPD_Prerequisite\python_3.7.6\Lib\site-packages\transitions\core.py", line 807, in _checked_assignment
    if hasattr(model, name):
TypeError: hasattr(): attribute name must be string

I can solve it by using Enum's .name:

    transitions = [
        {'trigger': Trigger.heat.name, 'source': State.SOLID, 'dest': State.LIQUID},
        {'trigger': Trigger.heat.name, 'source': State.LIQUID, 'dest': State.GAS},
        {'trigger': Trigger.cool.name, 'source': State.GAS, 'dest': State.LIQUID},
        {'trigger': Trigger.cool.name, 'source': State.LIQUID, 'dest': State.SOLID}
    ]

But the asymmetry between State and Trigger bothers me.
Am I doing something wrong? Why does Enum work for State but not Trigger?


Solution

  • As vish already pointed out, there is no way to use Enums as triggers directly. Transitions binds trigger names as methods to models and thus requires triggers to be strings. However, Machine and all subclasses have been written with inheritance in mind. If you want to use Enums instead of classes and class attributes, you can just derive a suitable EnumTransitionMachine and get a unified interface:

    from transitions import Machine
    from enum import Enum, auto
    
    
    class State(Enum):
        A = auto()
        B = auto()
        C = auto()
    
    
    class Transitions(Enum):
        GO = auto()
        PROCEED = auto()
    
    
    class EnumTransitionMachine(Machine):
    
        def add_transition(self, trigger, *args, **kwargs):
            super().add_transition(trigger.name.lower() if hasattr(trigger, 'name') else trigger, *args, **kwargs)
    
    
    transitions = [[Transitions.GO, State.A, State.B], [Transitions.PROCEED, State.B, State.C]]
    m = EnumTransitionMachine(states=State, transitions=transitions, initial=State.A)
    m.go()
    assert m.state == State.B
    m.proceed()
    assert m.is_C()
    

    FYI: there is also the possibility to use Enums with string values:

    class State(Enum):
        A = "A"
        B = "B"
        C = "C"
    
    
    class Transitions(Enum):
        GO = "GO"
        PROCEED = "PROCEED"
    
    # you could use the enum's value then instead:
        def add_transition(self, trigger, *args, **kwargs):
            super().add_transition(trigger.value.lower() if hasattr(trigger, 'value') else trigger, *args, **kwargs)