python-statemachine 2.3.1__py3-none-any.whl → 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
statemachine/factory.py CHANGED
@@ -185,7 +185,7 @@ class StateMachineMetaclass(type):
185
185
  cls.add_state(key, value)
186
186
  elif isinstance(value, (Transition, TransitionList)):
187
187
  cls.add_event(key, value)
188
- elif getattr(value, "_callbacks_to_update", None):
188
+ elif getattr(value, "_specs_to_update", None):
189
189
  cls._add_unbounded_callback(key, value)
190
190
 
191
191
  def _add_states_from_dict(cls, states):
@@ -193,16 +193,15 @@ class StateMachineMetaclass(type):
193
193
  cls.add_state(state_id, state)
194
194
 
195
195
  def _add_unbounded_callback(cls, attr_name, func):
196
- if func._is_event:
197
- # if func is an event, the `attr_name` will be replaced by an event trigger,
198
- # so we'll also give the ``func`` a new unique name to be used by the callback
199
- # machinery.
200
- cls.add_event(attr_name, func._transitions)
201
- attr_name = f"_{attr_name}_{uuid4().hex}"
202
- setattr(cls, attr_name, func)
203
-
204
- for ref in func._callbacks_to_update:
205
- ref(attr_name)
196
+ # if func is an event, the `attr_name` will be replaced by an event trigger,
197
+ # so we'll also give the ``func`` a new unique name to be used by the callback
198
+ # machinery.
199
+ cls.add_event(attr_name, func._transitions)
200
+ attr_name = f"_{attr_name}_{uuid4().hex}"
201
+ setattr(cls, attr_name, func)
202
+
203
+ for ref in func._specs_to_update:
204
+ ref(getattr(cls, attr_name), attr_name)
206
205
 
207
206
  def add_state(cls, id, state: State):
208
207
  state._set_id(id)
statemachine/mixins.py CHANGED
@@ -7,15 +7,18 @@ class MachineMixin:
7
7
  ``StateMachine``.
8
8
  """
9
9
 
10
- state_field_name = "state" # type: str
10
+ state_field_name: str = "state"
11
11
  """The model's state field name that will hold the state value."""
12
12
 
13
- state_machine_name = None # type: str
13
+ state_machine_name: "str | None" = None
14
14
  """A fully qualified name of the class, where it can be imported."""
15
15
 
16
- state_machine_attr = "statemachine" # type: str
16
+ state_machine_attr: str = "statemachine"
17
17
  """Name of the model's attribute that will hold the machine instance."""
18
18
 
19
+ bind_events_as_methods: bool = False
20
+ """If ``True`` the state machine events triggers will be bound to the model as methods."""
21
+
19
22
  def __init__(self, *args, **kwargs):
20
23
  super().__init__(*args, **kwargs)
21
24
  if not self.state_machine_name:
@@ -23,8 +26,11 @@ class MachineMixin:
23
26
  _("{!r} is not a valid state machine name.").format(self.state_machine_name)
24
27
  )
25
28
  machine_cls = registry.get_machine_cls(self.state_machine_name)
29
+ sm = machine_cls(self, state_field=self.state_field_name)
26
30
  setattr(
27
31
  self,
28
32
  self.state_machine_attr,
29
- machine_cls(self, state_field=self.state_field_name),
33
+ sm,
30
34
  )
35
+ if self.bind_events_as_methods:
36
+ sm.bind_events_to(self)
statemachine/registry.py CHANGED
@@ -1,14 +1,14 @@
1
1
  import warnings
2
2
 
3
+ from .utils import qualname
4
+
3
5
  try:
4
- _has_django = True
5
6
  from django.utils.module_loading import autodiscover_modules
6
- except ImportError:
7
+ except ImportError: # pragma: no cover
7
8
  # Not a django project
8
- autodiscover_modules = None
9
- _has_django = False
9
+ def autodiscover_modules(module_name: str):
10
+ pass
10
11
 
11
- from .utils import qualname
12
12
 
13
13
  _REGISTRY = {}
14
14
  _initialized = False
@@ -39,8 +39,5 @@ def init_registry():
39
39
 
40
40
 
41
41
  def load_modules(modules=None):
42
- if not _has_django:
43
- return
44
-
45
42
  for module in modules:
46
43
  autodiscover_modules(module)
statemachine/signature.py CHANGED
@@ -6,6 +6,7 @@ from inspect import iscoroutinefunction
6
6
  from itertools import chain
7
7
  from types import MethodType
8
8
  from typing import Any
9
+ from typing import Callable
9
10
 
10
11
 
11
12
  def _make_key(method):
@@ -44,7 +45,7 @@ def signature_cache(user_function):
44
45
 
45
46
  class SignatureAdapter(Signature):
46
47
  @classmethod
47
- def wrap(cls, method):
48
+ def wrap(cls, method) -> Callable:
48
49
  """Build a wrapper that adapts the received arguments to the inner ``method`` signature"""
49
50
 
50
51
  sig = cls.from_callable(method)
@@ -54,18 +55,18 @@ class SignatureAdapter(Signature):
54
55
 
55
56
  if iscoroutinefunction(method):
56
57
 
57
- async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
58
+ async def signature_adapter(*args: Any, **kwargs: Any) -> Any:
58
59
  ba = sig_bind_expected(*args, **kwargs)
59
60
  return await method(*ba.args, **ba.kwargs)
60
61
  else:
61
62
 
62
- async def method_wrapper(*args: Any, **kwargs: Any) -> Any:
63
+ def signature_adapter(*args: Any, **kwargs: Any) -> Any: # type: ignore[misc]
63
64
  ba = sig_bind_expected(*args, **kwargs)
64
65
  return method(*ba.args, **ba.kwargs)
65
66
 
66
- method_wrapper.__name__ = metadata_to_copy.__name__
67
+ signature_adapter.__name__ = metadata_to_copy.__name__
67
68
 
68
- return method_wrapper
69
+ return signature_adapter
69
70
 
70
71
  @classmethod
71
72
  @signature_cache
statemachine/state.py CHANGED
@@ -3,8 +3,9 @@ from typing import Any
3
3
  from typing import Dict
4
4
  from weakref import ref
5
5
 
6
- from .callbacks import CallbackMetaList
6
+ from .callbacks import CallbackGroup
7
7
  from .callbacks import CallbackPriority
8
+ from .callbacks import CallbackSpecList
8
9
  from .exceptions import StateMachineError
9
10
  from .i18n import _
10
11
  from .transition import Transition
@@ -108,8 +109,13 @@ class State:
108
109
  self._final = final
109
110
  self._id: str = ""
110
111
  self.transitions = TransitionList()
111
- self.enter = CallbackMetaList().add(enter, priority=CallbackPriority.INLINE)
112
- self.exit = CallbackMetaList().add(exit, priority=CallbackPriority.INLINE)
112
+ self._specs = CallbackSpecList()
113
+ self.enter = self._specs.grouper(CallbackGroup.ENTER).add(
114
+ enter, priority=CallbackPriority.INLINE
115
+ )
116
+ self.exit = self._specs.grouper(CallbackGroup.EXIT).add(
117
+ exit, priority=CallbackPriority.INLINE
118
+ )
113
119
 
114
120
  def __eq__(self, other):
115
121
  return isinstance(other, State) and self.name == other.name and self.id == other.id
@@ -118,20 +124,10 @@ class State:
118
124
  return hash(repr(self))
119
125
 
120
126
  def _setup(self):
121
- self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
122
- self.enter.add(
123
- f"on_enter_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True
124
- )
125
- self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, suppress_errors=True)
126
- self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, suppress_errors=True)
127
-
128
- def _add_observer(self, register):
129
- register(self.enter)
130
- register(self.exit)
131
-
132
- def _check_callbacks(self, check):
133
- check(self.enter)
134
- check(self.exit)
127
+ self.enter.add("on_enter_state", priority=CallbackPriority.GENERIC, is_convention=True)
128
+ self.enter.add(f"on_enter_{self.id}", priority=CallbackPriority.NAMING, is_convention=True)
129
+ self.exit.add("on_exit_state", priority=CallbackPriority.GENERIC, is_convention=True)
130
+ self.exit.add(f"on_exit_{self.id}", priority=CallbackPriority.NAMING, is_convention=True)
135
131
 
136
132
  def __repr__(self):
137
133
  return (
@@ -1,20 +1,24 @@
1
+ import warnings
1
2
  from collections import deque
2
3
  from copy import deepcopy
3
4
  from functools import partial
5
+ from inspect import isawaitable
6
+ from threading import Lock
4
7
  from typing import TYPE_CHECKING
5
8
  from typing import Any
6
9
  from typing import Dict
10
+ from typing import List
7
11
 
8
12
  from statemachine.graph import iterate_states_and_transitions
9
13
  from statemachine.utils import run_async_from_sync
10
14
 
11
- from .callbacks import CallbackMetaList
12
15
  from .callbacks import CallbacksExecutor
13
16
  from .callbacks import CallbacksRegistry
14
- from .dispatcher import ObjectConfig
15
- from .dispatcher import resolver_factory
17
+ from .dispatcher import Listener
18
+ from .dispatcher import Listeners
19
+ from .engines.async_ import AsyncEngine
20
+ from .engines.sync import SyncEngine
16
21
  from .event import Event
17
- from .event_data import EventData
18
22
  from .event_data import TriggerData
19
23
  from .exceptions import InvalidDefinition
20
24
  from .exceptions import InvalidStateValue
@@ -22,7 +26,6 @@ from .exceptions import TransitionNotAllowed
22
26
  from .factory import StateMachineMetaclass
23
27
  from .i18n import _
24
28
  from .model import Model
25
- from .transition import Transition
26
29
 
27
30
  if TYPE_CHECKING:
28
31
  from .state import State
@@ -49,6 +52,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
49
52
  :ref:`transition`, including tolerance to unknown :ref:`event` triggers.
50
53
  Default: ``False``.
51
54
 
55
+ listeners: An optional list of objects that provies attributes to be used as callbacks.
56
+ See :ref:`listeners` for more details.
57
+
52
58
  """
53
59
 
54
60
  TransitionNotAllowed = TransitionNotAllowed
@@ -69,28 +75,50 @@ class StateMachine(metaclass=StateMachineMetaclass):
69
75
  start_value: Any = None,
70
76
  rtc: bool = True,
71
77
  allow_event_without_transition: bool = False,
78
+ listeners: "List[object] | None" = None,
72
79
  ):
73
80
  self.model = model if model else Model()
74
81
  self.state_field = state_field
75
82
  self.start_value = start_value
76
83
  self.allow_event_without_transition = allow_event_without_transition
77
- self.__initialized = False
78
- self.__rtc = rtc
79
- self.__processing: bool = False
80
84
  self._external_queue: deque = deque()
81
85
  self._callbacks_registry = CallbacksRegistry()
82
86
  self._states_for_instance: Dict[State, State] = {}
83
- self._observers: Dict[Any, Any] = {}
87
+
88
+ self._listeners: Dict[Any, Any] = {}
89
+ """Listeners that provides attributes to be used as callbacks."""
84
90
 
85
91
  if self._abstract:
86
92
  raise InvalidDefinition(_("There are no states or transitions."))
87
93
 
88
- self._register_callbacks()
94
+ self._register_callbacks(listeners or [])
89
95
 
90
96
  # Activate the initial state, this only works if the outer scope is sync code.
91
97
  # for async code, the user should manually call `await sm.activate_initial_state()`
92
98
  # after state machine creation.
93
- run_async_from_sync(self.activate_initial_state())
99
+ if self.current_state_value is None:
100
+ trigger_data = TriggerData(
101
+ machine=self,
102
+ event="__initial__",
103
+ )
104
+ self._put_nonblocking(trigger_data)
105
+
106
+ self._engine = self._get_engine(rtc)
107
+
108
+ def _get_engine(self, rtc: bool):
109
+ if self._callbacks_registry._method_types[True] > 0:
110
+ return AsyncEngine(self, rtc=rtc)
111
+ else:
112
+ return SyncEngine(self, rtc=rtc)
113
+
114
+ def activate_initial_state(self):
115
+ result = self._engine.activate_initial_state()
116
+ if not isawaitable(result):
117
+ return result
118
+ return run_async_from_sync(result)
119
+
120
+ def _processing_loop(self):
121
+ return self._engine._processing_loop()
94
122
 
95
123
  def __init_subclass__(cls, strict_states: bool = False):
96
124
  cls._strict_states = strict_states
@@ -110,15 +138,20 @@ class StateMachine(metaclass=StateMachineMetaclass):
110
138
 
111
139
  def __deepcopy__(self, memo):
112
140
  deepcopy_method = self.__deepcopy__
113
- self.__deepcopy__ = None
114
- try:
115
- cp = deepcopy(self, memo)
116
- finally:
117
- self.__deepcopy__ = deepcopy_method
118
- cp.__deepcopy__ = deepcopy_method
141
+ lock = self._engine._processing
142
+ with lock:
143
+ self.__deepcopy__ = None
144
+ self._engine._processing = None
145
+ try:
146
+ cp = deepcopy(self, memo)
147
+ cp._engine._processing = Lock()
148
+ finally:
149
+ self.__deepcopy__ = deepcopy_method
150
+ cp.__deepcopy__ = deepcopy_method
151
+ self._engine._processing = lock
119
152
  cp._callbacks_registry.clear()
120
- cp._register_callbacks()
121
- cp.add_observer(*cp._observers.keys())
153
+ cp._register_callbacks([])
154
+ cp.add_listener(*cp._listeners.keys())
122
155
  return cp
123
156
 
124
157
  def _get_initial_state(self):
@@ -128,79 +161,75 @@ class StateMachine(metaclass=StateMachineMetaclass):
128
161
  except KeyError as err:
129
162
  raise InvalidStateValue(current_state_value) from err
130
163
 
131
- async def activate_initial_state(self):
132
- """
133
- Activate the initial state.
134
-
135
- Called automatically on state machine creation from sync code, but in
136
- async code, the user must call this method explicitly.
137
-
138
- Given how async works on python, there's no built-in way to activate the initial state that
139
- may depend on async code from the StateMachine.__init__ method.
140
-
141
- We do a `_ensure_is_initialized()` check before each event, but to check the current state
142
- just before the state machine is created, the user must await the activation of the initial
143
- state explicitly.
144
- """
145
- if self.__initialized:
146
- return
147
- self.__initialized = True
148
- if self.current_state_value is None:
149
- # send an one-time event `__initial__` to enter the current state.
150
- # current_state = self.current_state
151
- initial_transition = Transition(None, self._get_initial_state(), event="__initial__")
152
- initial_transition.before.clear()
153
- initial_transition.on.clear()
154
- initial_transition.after.clear()
155
-
156
- event_data = EventData(
157
- trigger_data=TriggerData(
158
- machine=self,
159
- event=initial_transition.event,
160
- ),
161
- transition=initial_transition,
162
- )
163
- await self._activate(event_data)
164
+ def bind_events_to(self, *targets):
165
+ """Bind the state machine events to the target objects."""
166
+
167
+ for event in self.events:
168
+ trigger = getattr(self, event.name)
169
+ for target in targets:
170
+ if hasattr(target, event.name):
171
+ warnings.warn(
172
+ f"Attribute {event.name!r} already exists on {target!r}. "
173
+ f"Skipping binding.",
174
+ UserWarning,
175
+ stacklevel=2,
176
+ )
177
+ continue
178
+ setattr(target, event.name, trigger)
179
+
180
+ def _add_listener(self, listeners: "Listeners"):
181
+ register = partial(listeners.resolve, registry=self._callbacks_registry)
182
+ for visited in iterate_states_and_transitions(self.states):
183
+ register(visited._specs)
164
184
 
165
- async def _ensure_is_initialized(self):
166
- await self.activate_initial_state()
185
+ return self
167
186
 
168
- def _register_callbacks(self):
169
- self._add_observer(
170
- (
171
- ObjectConfig.from_obj(self, skip_attrs=self._protected_attrs),
172
- ObjectConfig.from_obj(self.model, skip_attrs={self.state_field}),
187
+ def _register_callbacks(self, listeners: List[object]):
188
+ self._listeners.update({listener: None for listener in listeners})
189
+ self._add_listener(
190
+ Listeners.from_listeners(
191
+ (
192
+ Listener.from_obj(self, skip_attrs=self._protected_attrs),
193
+ Listener.from_obj(self.model, skip_attrs={self.state_field}),
194
+ *(Listener.from_obj(listener) for listener in listeners),
195
+ )
173
196
  )
174
197
  )
175
198
 
176
199
  check_callbacks = self._callbacks_registry.check
177
200
  for visited in iterate_states_and_transitions(self.states):
178
201
  try:
179
- visited._check_callbacks(check_callbacks)
202
+ check_callbacks(visited._specs)
180
203
  except Exception as err:
181
204
  raise InvalidDefinition(
182
205
  f"Error on {visited!s} when resolving callbacks: {err}"
183
206
  ) from err
184
207
 
185
- def _add_observer(self, observers):
186
- register = partial(self._callbacks_registry.register, resolver=resolver_factory(observers))
187
- for visited in iterate_states_and_transitions(self.states):
188
- visited._add_observer(register)
189
-
190
- return self
208
+ self._callbacks_registry.async_or_sync()
191
209
 
192
210
  def add_observer(self, *observers):
193
- """Add an observer.
211
+ """Add a listener."""
212
+ warnings.warn(
213
+ """The `add_observer` was rebranded to `add_listener`.""",
214
+ DeprecationWarning,
215
+ stacklevel=2,
216
+ )
217
+ return self.add_listener(*observers)
218
+
219
+ def add_listener(self, *listeners):
220
+ """Add a listener.
194
221
 
195
- Observers are a way to generically add behavior to a :ref:`StateMachine` without changing
222
+ Listener are a way to generically add behavior to a :ref:`StateMachine` without changing
196
223
  its internal implementation.
197
224
 
198
225
  .. seealso::
199
226
 
200
- :ref:`observers`.
227
+ :ref:`listeners`.
201
228
  """
202
- self._observers.update({o: None for o in observers})
203
- return self._add_observer(tuple(ObjectConfig.from_obj(o) for o in observers))
229
+ self._listeners.update({o: None for o in listeners})
230
+ return self._add_listener(
231
+ Listeners.from_listeners(Listener.from_obj(o) for o in listeners)
232
+ )
204
233
 
205
234
  def _repr_html_(self):
206
235
  return f'<div class="statemachine">{self._repr_svg_()}</div>'
@@ -267,97 +296,9 @@ class StateMachine(metaclass=StateMachineMetaclass):
267
296
  """List of the current allowed events."""
268
297
  return [getattr(self, event) for event in self.current_state.transitions.unique_events]
269
298
 
270
- async def _process(self, trigger):
271
- """Process event triggers.
272
-
273
- The simplest implementation is the non-RTC (synchronous),
274
- where the trigger will be run immediately and the result collected as the return.
275
-
276
- .. note::
277
-
278
- While processing the trigger, if others events are generated, they
279
- will also be processed immediately, so a "nested" behavior happens.
280
-
281
- If the machine is on ``rtc`` model (queued), the event is put on a queue, and only the
282
- first event will have the result collected.
283
-
284
- .. note::
285
- While processing the queue items, if others events are generated, they
286
- will be processed sequentially (and not nested).
287
-
288
- """
289
- if not self.__rtc:
290
- # The machine is in "synchronous" mode
291
- return await trigger()
292
-
293
- # The machine is in "queued" mode
294
- # Add the trigger to queue and start processing in a loop.
295
- self._external_queue.append(trigger)
296
-
297
- # We make sure that only the first event enters the processing critical section,
298
- # next events will only be put on the queue and processed by the same loop.
299
- if self.__processing:
300
- return
301
-
302
- return await self._processing_loop()
303
-
304
- async def _processing_loop(self):
305
- """Execute the triggers in the queue in order until the queue is empty"""
306
- self.__processing = True
307
-
308
- # We will collect the first result as the processing result to keep backwards compatibility
309
- # so we need to use a sentinel object instead of `None` because the first result may
310
- # be also `None`, and on this case the `first_result` may be overridden by another result.
311
- sentinel = object()
312
- first_result = sentinel
313
- try:
314
- while self._external_queue:
315
- trigger = self._external_queue.popleft()
316
- try:
317
- result = await trigger()
318
- if first_result is sentinel:
319
- first_result = result
320
- except Exception:
321
- # Whe clear the queue as we don't have an expected behavior
322
- # and cannot keep processing
323
- self._external_queue.clear()
324
- raise
325
- finally:
326
- self.__processing = False
327
- return first_result if first_result is not sentinel else None
328
-
329
- async def _activate(self, event_data: EventData):
330
- transition = event_data.transition
331
- source = event_data.state
332
- target = transition.target
333
-
334
- result = await self._callbacks(transition.before).call(
335
- *event_data.args, **event_data.extended_kwargs
336
- )
337
- if source is not None and not transition.internal:
338
- await self._callbacks(source.exit).call(*event_data.args, **event_data.extended_kwargs)
339
-
340
- result += await self._callbacks(transition.on).call(
341
- *event_data.args, **event_data.extended_kwargs
342
- )
343
-
344
- self.current_state = target
345
- event_data.state = target
346
-
347
- if not transition.internal:
348
- await self._callbacks(target.enter).call(
349
- *event_data.args, **event_data.extended_kwargs
350
- )
351
- await self._callbacks(transition.after).call(
352
- *event_data.args, **event_data.extended_kwargs
353
- )
354
-
355
- if len(result) == 0:
356
- result = None
357
- elif len(result) == 1:
358
- result = result[0]
359
-
360
- return result
299
+ def _put_nonblocking(self, trigger_data: TriggerData):
300
+ """Put the trigger on the queue without blocking the caller."""
301
+ self._external_queue.append(trigger_data)
361
302
 
362
303
  def send(self, event: str, *args, **kwargs):
363
304
  """Send an :ref:`Event` to the state machine.
@@ -370,9 +311,12 @@ class StateMachine(metaclass=StateMachineMetaclass):
370
311
  See: :ref:`triggering events`.
371
312
 
372
313
  """
373
- return run_async_from_sync(self.async_send(event, *args, **kwargs))
314
+ result = self._async_send(event, *args, **kwargs)
315
+ if not isawaitable(result):
316
+ return result
317
+ return run_async_from_sync(result)
374
318
 
375
- async def async_send(self, event: str, *args, **kwargs):
319
+ def _async_send(self, event: str, *args, **kwargs):
376
320
  """Send an :ref:`Event` to the state machine.
377
321
 
378
322
  .. seealso::
@@ -381,7 +325,7 @@ class StateMachine(metaclass=StateMachineMetaclass):
381
325
 
382
326
  """
383
327
  event_instance: Event = Event(event)
384
- return await event_instance.trigger(self, *args, **kwargs)
328
+ return event_instance.trigger(self, *args, **kwargs)
385
329
 
386
- def _callbacks(self, meta_list: CallbackMetaList) -> CallbacksExecutor:
387
- return self._callbacks_registry[meta_list]
330
+ def _get_callbacks(self, key) -> CallbacksExecutor:
331
+ return self._callbacks_registry[key]
statemachine/states.py CHANGED
@@ -54,6 +54,7 @@ class States:
54
54
  return list(self) == list(other)
55
55
 
56
56
  def __getattr__(self, name: str):
57
+ name = name.lower()
57
58
  if name in self._states:
58
59
  return self._states[name]
59
60
  raise AttributeError(f"{name} not found in {self.__class__.__name__}")
@@ -80,7 +81,7 @@ class States:
80
81
  return self._states.items()
81
82
 
82
83
  @classmethod
83
- def from_enum(cls, enum_type: EnumType, initial: Enum, final=None):
84
+ def from_enum(cls, enum_type: EnumType, initial, final=None):
84
85
  """
85
86
  Creates a new instance of the ``States`` class from an enumeration.
86
87
 
@@ -124,7 +125,7 @@ class States:
124
125
  True
125
126
 
126
127
  >>> sm.current_state_value
127
- 2
128
+ <Status.completed: 2>
128
129
 
129
130
  Args:
130
131
  enum_type: An enumeration containing the states of the machine.
@@ -137,7 +138,7 @@ class States:
137
138
  final_set = set(ensure_iterable(final))
138
139
  return cls(
139
140
  {
140
- e.name: State(value=e.value, initial=e is initial, final=e in final_set)
141
+ e.name.lower(): State(value=e, initial=e is initial, final=e in final_set)
141
142
  for e in enum_type
142
143
  }
143
144
  )