3

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?

Gulzar
  • 23,452
  • 27
  • 113
  • 201

2 Answers2

2

The error says it correctly:

TypeError: hasattr(): attribute name must be string. If you go through enum documentation (https://docs.python.org/3/library/enum.html), you will see what each of the functions return. You can override them and see if that works. Unless the code in Machine checks specifically for Enum and converts it to string, it would not work.

Simpler way to make it work, in a sort of way you want it is do following instead:

class Trigger:
    HEAT = 'HEAT'
    COOL = 'COOL'

2

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)
aleneum
  • 2,083
  • 12
  • 29