python-statemachine 3.1.1__py3-none-any.whl → 3.2.0__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.
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
- python_statemachine-3.2.0.dist-info/RECORD +72 -0
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/extract.py +23 -24
- statemachine/contrib/diagram/formatter.py +5 -7
- statemachine/contrib/diagram/model.py +9 -11
- statemachine/contrib/diagram/renderers/dot.py +20 -26
- statemachine/contrib/diagram/renderers/mermaid.py +36 -40
- statemachine/contrib/diagram/renderers/table.py +7 -9
- statemachine/contrib/weighted.py +7 -11
- statemachine/dispatcher.py +13 -12
- statemachine/engines/async_.py +5 -6
- statemachine/engines/base.py +12 -14
- statemachine/event.py +1 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +12 -16
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +45 -225
- statemachine/io/{scxml/actions.py → actions.py} +158 -288
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +275 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +161 -0
- statemachine/io/model.py +268 -0
- statemachine/io/native.py +402 -0
- statemachine/io/ports.py +83 -0
- statemachine/io/schemas/statechart.schema.json +258 -0
- statemachine/io/scxml/__init__.py +12 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +64 -47
- statemachine/io/system_variables.py +184 -0
- statemachine/io/validation.py +44 -0
- statemachine/io/yaml/__init__.py +1 -0
- statemachine/io/yaml/reader.py +65 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +20 -22
- statemachine/orderedset.py +3 -3
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +171 -42
- statemachine/state.py +5 -6
- statemachine/statemachine.py +18 -20
- statemachine/states.py +3 -5
- statemachine/transition.py +3 -4
- statemachine/transition_list.py +4 -5
- statemachine/transition_mixin.py +1 -1
- python_statemachine-3.1.1.dist-info/RECORD +0 -58
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Compile the neutral IR into StateChart class keyword arguments.
|
|
2
|
+
|
|
3
|
+
:class:`DefinitionBuilder` translates a :class:`~statemachine.io.model.StateMachineDefinition`
|
|
4
|
+
into the ``kwargs`` accepted by :func:`~statemachine.io.create_machine_class_from_definition`,
|
|
5
|
+
compiling guards and executable content through an
|
|
6
|
+
:class:`~statemachine.io.evaluators.Evaluator` (secure by default).
|
|
7
|
+
|
|
8
|
+
It is format-neutral and runtime-agnostic: anything that is a runtime concern
|
|
9
|
+
(system-variable injection, the invoke bootstrap, building invokers) is requested from a
|
|
10
|
+
:class:`RuntimeHooks` collaborator, so the builder never imports the concrete runtime and
|
|
11
|
+
there is no import cycle.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
from typing import Protocol
|
|
16
|
+
|
|
17
|
+
from .actions import Cond
|
|
18
|
+
from .actions import DoneDataCallable
|
|
19
|
+
from .actions import ExecuteBlock
|
|
20
|
+
from .actions import create_datamodel_action_callable
|
|
21
|
+
from .class_factory import HistoryDefinition
|
|
22
|
+
from .class_factory import StateDefinition
|
|
23
|
+
from .class_factory import TransitionDict
|
|
24
|
+
from .class_factory import TransitionsList
|
|
25
|
+
from .evaluators import Evaluator
|
|
26
|
+
from .model import HistoryState
|
|
27
|
+
from .model import InvokeDefinition
|
|
28
|
+
from .model import State
|
|
29
|
+
from .model import StateMachineDefinition
|
|
30
|
+
from .model import Transition
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RuntimeHooks(Protocol):
|
|
34
|
+
"""Runtime concerns the builder delegates back to the interpreter."""
|
|
35
|
+
|
|
36
|
+
def initial_enter_prefix(
|
|
37
|
+
self, is_invoked: bool
|
|
38
|
+
) -> list: # pragma: no cover - structural Protocol
|
|
39
|
+
"""Callbacks to insert at the front of the initial state's ``enter`` list."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def definition_kwargs(self, definition) -> dict: # pragma: no cover - structural Protocol
|
|
43
|
+
"""Extra kwargs for ``create_machine_class_from_definition`` (e.g. ``prepare_event``)."""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
def make_invoker(
|
|
47
|
+
self, invoke_def: InvokeDefinition
|
|
48
|
+
) -> Any: # pragma: no cover - structural Protocol
|
|
49
|
+
"""Build the invoke handler for an ``<invoke>`` definition."""
|
|
50
|
+
...
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DefinitionBuilder:
|
|
54
|
+
"""Translates the neutral IR into ``create_machine_class_from_definition`` kwargs."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, *, evaluator: Evaluator, hooks: RuntimeHooks):
|
|
57
|
+
self._evaluator = evaluator
|
|
58
|
+
self._hooks = hooks
|
|
59
|
+
|
|
60
|
+
def build_class_kwargs(self, definition: StateMachineDefinition, *, is_invoked: bool) -> dict:
|
|
61
|
+
states_dict = self._process_states(definition.states)
|
|
62
|
+
|
|
63
|
+
# Find the initial state for inserting init callbacks
|
|
64
|
+
try:
|
|
65
|
+
initial_state = next(s for s in iter(states_dict.values()) if s.get("initial"))
|
|
66
|
+
except StopIteration:
|
|
67
|
+
initial_state = next(iter(states_dict.values()))
|
|
68
|
+
|
|
69
|
+
if "enter" not in initial_state:
|
|
70
|
+
initial_state["enter"] = []
|
|
71
|
+
|
|
72
|
+
insert_pos = 0
|
|
73
|
+
# Runtime callbacks that must run before the datamodel (e.g. invoke init).
|
|
74
|
+
for callback in self._hooks.initial_enter_prefix(is_invoked):
|
|
75
|
+
initial_state["enter"].insert(insert_pos, callback) # type: ignore[union-attr]
|
|
76
|
+
insert_pos += 1
|
|
77
|
+
|
|
78
|
+
# Process datamodel (initial variables)
|
|
79
|
+
if definition.datamodel:
|
|
80
|
+
datamodel = create_datamodel_action_callable(definition.datamodel, self._evaluator)
|
|
81
|
+
if datamodel: # pragma: no branch – parse guarantees non-empty
|
|
82
|
+
if isinstance( # pragma: no branch – always a list from lines above
|
|
83
|
+
initial_state["enter"], list
|
|
84
|
+
):
|
|
85
|
+
initial_state["enter"].insert(insert_pos, datamodel) # type: ignore[arg-type]
|
|
86
|
+
|
|
87
|
+
return {"states": states_dict, **self._hooks.definition_kwargs(definition)}
|
|
88
|
+
|
|
89
|
+
def _process_history(self, history: dict[str, HistoryState]) -> dict[str, HistoryDefinition]:
|
|
90
|
+
states_dict: dict[str, HistoryDefinition] = {}
|
|
91
|
+
for state_id, state in history.items():
|
|
92
|
+
state_dict = HistoryDefinition()
|
|
93
|
+
state_dict["type"] = state.type
|
|
94
|
+
if state.transitions:
|
|
95
|
+
state_dict["transitions"] = self._process_transitions(state.transitions)
|
|
96
|
+
states_dict[state_id] = state_dict
|
|
97
|
+
return states_dict
|
|
98
|
+
|
|
99
|
+
def _process_states(self, states: dict[str, State]) -> dict[str, StateDefinition]:
|
|
100
|
+
return {state_id: self._process_state(state) for state_id, state in states.items()}
|
|
101
|
+
|
|
102
|
+
def _process_state(self, state: State) -> StateDefinition: # noqa: C901
|
|
103
|
+
state_dict = StateDefinition()
|
|
104
|
+
if state.initial:
|
|
105
|
+
state_dict["initial"] = True
|
|
106
|
+
if state.final:
|
|
107
|
+
state_dict["final"] = True
|
|
108
|
+
if state.parallel:
|
|
109
|
+
state_dict["parallel"] = True
|
|
110
|
+
|
|
111
|
+
# Process enter actions (executable content first, then callback refs)
|
|
112
|
+
enter_callables: list = [
|
|
113
|
+
ExecuteBlock(content, self._evaluator)
|
|
114
|
+
for content in state.onentry
|
|
115
|
+
if not content.is_empty
|
|
116
|
+
]
|
|
117
|
+
enter_callables.extend(state.enter_refs)
|
|
118
|
+
if enter_callables:
|
|
119
|
+
state_dict["enter"] = enter_callables
|
|
120
|
+
if state.final and state.donedata:
|
|
121
|
+
state_dict["donedata"] = DoneDataCallable(state.donedata, self._evaluator)
|
|
122
|
+
|
|
123
|
+
# Process exit actions (executable content first, then callback refs)
|
|
124
|
+
exit_callables: list = [
|
|
125
|
+
ExecuteBlock(content, self._evaluator)
|
|
126
|
+
for content in state.onexit
|
|
127
|
+
if not content.is_empty
|
|
128
|
+
]
|
|
129
|
+
exit_callables.extend(state.exit_refs)
|
|
130
|
+
if exit_callables:
|
|
131
|
+
state_dict["exit"] = exit_callables
|
|
132
|
+
|
|
133
|
+
# Process transitions
|
|
134
|
+
if state.transitions:
|
|
135
|
+
state_dict["transitions"] = self._process_transitions(state.transitions)
|
|
136
|
+
|
|
137
|
+
# Process invoke elements (delegated to the runtime)
|
|
138
|
+
if state.invocations:
|
|
139
|
+
invokers = [self._hooks.make_invoker(inv) for inv in state.invocations]
|
|
140
|
+
state_dict["invoke"] = invokers # type: ignore[typeddict-unknown-key]
|
|
141
|
+
|
|
142
|
+
if state.states:
|
|
143
|
+
state_dict["states"] = self._process_states(state.states)
|
|
144
|
+
|
|
145
|
+
if state.history:
|
|
146
|
+
state_dict["history"] = self._process_history(state.history)
|
|
147
|
+
|
|
148
|
+
return state_dict
|
|
149
|
+
|
|
150
|
+
def _process_transitions(self, transitions: list[Transition]):
|
|
151
|
+
result: TransitionsList = []
|
|
152
|
+
for transition in transitions:
|
|
153
|
+
event = transition.event or None
|
|
154
|
+
transition_dict: TransitionDict = {
|
|
155
|
+
"event": event,
|
|
156
|
+
"target": transition.target,
|
|
157
|
+
"internal": transition.internal,
|
|
158
|
+
"initial": transition.initial,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Process guards (cond / unless) as compiled expressions
|
|
162
|
+
if transition.cond:
|
|
163
|
+
transition_dict["cond"] = self._compile_guard(transition.cond)
|
|
164
|
+
if transition.unless:
|
|
165
|
+
transition_dict["unless"] = self._compile_guard(transition.unless)
|
|
166
|
+
|
|
167
|
+
# Each callback slot is executable content (compiled first) followed by the
|
|
168
|
+
# callback references. `on` mirrors SCXML; `before`/`after` are native-only
|
|
169
|
+
# library lifecycle slots that accept the same vocabulary.
|
|
170
|
+
on = self._lifecycle_callables(transition.on, transition.on_refs)
|
|
171
|
+
if on:
|
|
172
|
+
transition_dict["on"] = on if len(on) > 1 else on[0]
|
|
173
|
+
before = self._lifecycle_callables(transition.before, transition.before_refs)
|
|
174
|
+
if before:
|
|
175
|
+
transition_dict["before"] = before if len(before) > 1 else before[0]
|
|
176
|
+
after = self._lifecycle_callables(transition.after, transition.after_refs)
|
|
177
|
+
if after:
|
|
178
|
+
transition_dict["after"] = after if len(after) > 1 else after[0]
|
|
179
|
+
|
|
180
|
+
result.append(transition_dict)
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
def _lifecycle_callables(self, content, refs: list) -> list:
|
|
184
|
+
"""Compose a transition callback slot: executable content (if any) then named refs."""
|
|
185
|
+
callables: list = []
|
|
186
|
+
if content and not content.is_empty:
|
|
187
|
+
callables.append(ExecuteBlock(content, self._evaluator))
|
|
188
|
+
callables.extend(refs)
|
|
189
|
+
return callables
|
|
190
|
+
|
|
191
|
+
def _compile_guard(self, expr) -> Any:
|
|
192
|
+
"""Compile a guard expression (or list of them) into Cond callables."""
|
|
193
|
+
if isinstance(expr, (list, tuple)):
|
|
194
|
+
return [Cond.create(e, self._evaluator) for e in expr]
|
|
195
|
+
return Cond.create(expr, self._evaluator)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Build a :class:`~statemachine.statemachine.StateChart` class from a dict definition.
|
|
2
|
+
|
|
3
|
+
This is the lowest layer of the IO stack: it turns a plain mapping (the ``TransitionDict`` /
|
|
4
|
+
``StateDefinition`` schema declared here) into a concrete ``StateChart`` subclass via the
|
|
5
|
+
:class:`~statemachine.factory.StateMachineMetaclass`. The higher layers (the format readers,
|
|
6
|
+
the :class:`~statemachine.io.builder.DefinitionBuilder`, the
|
|
7
|
+
:class:`~statemachine.io.interpreter.Interpreter`) compile their neutral IR down to this
|
|
8
|
+
schema and call :func:`create_machine_class_from_definition`.
|
|
9
|
+
|
|
10
|
+
It depends only on the core (``factory``/``state``/``statemachine``/``transition``), never on
|
|
11
|
+
the loader stack, so importing it never triggers an import cycle.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
from typing import Any
|
|
17
|
+
from typing import Protocol
|
|
18
|
+
from typing import TypedDict
|
|
19
|
+
from typing import cast
|
|
20
|
+
|
|
21
|
+
from ..factory import StateMachineMetaclass
|
|
22
|
+
from ..state import HistoryState
|
|
23
|
+
from ..state import State
|
|
24
|
+
from ..statemachine import StateChart
|
|
25
|
+
from ..transition import Transition
|
|
26
|
+
from ..transition_list import TransitionList
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ActionProtocol(Protocol): # pragma: no cover
|
|
30
|
+
def __call__(self, *args, **kwargs) -> Any: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TransitionDict(TypedDict, total=False):
|
|
34
|
+
target: "str | None"
|
|
35
|
+
event: "str | None"
|
|
36
|
+
internal: bool
|
|
37
|
+
initial: bool
|
|
38
|
+
validators: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
39
|
+
cond: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
40
|
+
unless: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
41
|
+
on: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
42
|
+
before: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
43
|
+
after: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
TransitionsDict = dict["str | None", list[TransitionDict]]
|
|
47
|
+
TransitionsList = list[TransitionDict]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BaseStateKwargs(TypedDict, total=False):
|
|
51
|
+
name: str
|
|
52
|
+
value: Any
|
|
53
|
+
initial: bool
|
|
54
|
+
final: bool
|
|
55
|
+
parallel: bool
|
|
56
|
+
enter: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
57
|
+
exit: "str | ActionProtocol | Sequence[str] | Sequence[ActionProtocol]"
|
|
58
|
+
donedata: "ActionProtocol | None"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class StateKwargs(BaseStateKwargs, total=False):
|
|
62
|
+
states: list[State]
|
|
63
|
+
history: list[HistoryState]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class HistoryKwargs(TypedDict, total=False):
|
|
67
|
+
name: str
|
|
68
|
+
value: Any
|
|
69
|
+
type: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HistoryDefinition(HistoryKwargs, total=False):
|
|
73
|
+
on: TransitionsDict
|
|
74
|
+
transitions: TransitionsList
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class StateDefinition(BaseStateKwargs, total=False):
|
|
78
|
+
states: dict[str, "StateDefinition"]
|
|
79
|
+
history: dict[str, "HistoryDefinition"]
|
|
80
|
+
on: TransitionsDict
|
|
81
|
+
transitions: TransitionsList
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_history(
|
|
85
|
+
states: Mapping[str, "HistoryKwargs |HistoryDefinition"],
|
|
86
|
+
) -> tuple[dict[str, HistoryState], dict[str, dict]]:
|
|
87
|
+
states_instances: dict[str, HistoryState] = {}
|
|
88
|
+
events_definitions: dict[str, dict] = {}
|
|
89
|
+
for state_id, state_definition in states.items():
|
|
90
|
+
state_definition = cast(HistoryDefinition, state_definition)
|
|
91
|
+
transition_defs = state_definition.pop("on", {})
|
|
92
|
+
transition_list = state_definition.pop("transitions", [])
|
|
93
|
+
if transition_list:
|
|
94
|
+
transition_defs[None] = transition_list
|
|
95
|
+
|
|
96
|
+
if transition_defs:
|
|
97
|
+
events_definitions[state_id] = transition_defs
|
|
98
|
+
|
|
99
|
+
state_definition = cast(HistoryKwargs, state_definition)
|
|
100
|
+
states_instances[state_id] = HistoryState(**state_definition)
|
|
101
|
+
|
|
102
|
+
return (states_instances, events_definitions)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_states(
|
|
106
|
+
states: Mapping[str, "BaseStateKwargs | StateDefinition"],
|
|
107
|
+
) -> tuple[dict[str, State], dict[str, dict]]:
|
|
108
|
+
states_instances: dict[str, State] = {}
|
|
109
|
+
events_definitions: dict[str, dict] = {}
|
|
110
|
+
|
|
111
|
+
for state_id, state_definition in states.items():
|
|
112
|
+
# Process nested states. Replaces `states` as a definition by a list of `State` instances.
|
|
113
|
+
state_definition = cast(StateDefinition, state_definition)
|
|
114
|
+
|
|
115
|
+
# pop the nested states, history and transitions definitions
|
|
116
|
+
inner_states_defs: dict[str, StateDefinition] = state_definition.pop("states", {})
|
|
117
|
+
inner_history_defs: dict[str, HistoryDefinition] = state_definition.pop("history", {})
|
|
118
|
+
transition_defs = state_definition.pop("on", {})
|
|
119
|
+
transition_list = state_definition.pop("transitions", [])
|
|
120
|
+
if transition_list:
|
|
121
|
+
transition_defs[None] = transition_list
|
|
122
|
+
|
|
123
|
+
if inner_states_defs:
|
|
124
|
+
inner_states, inner_events = _parse_states(inner_states_defs)
|
|
125
|
+
|
|
126
|
+
top_level_states = [
|
|
127
|
+
state._set_id(state_id)
|
|
128
|
+
for state_id, state in inner_states.items()
|
|
129
|
+
if not state.parent
|
|
130
|
+
]
|
|
131
|
+
state_definition["states"] = top_level_states # type: ignore
|
|
132
|
+
states_instances.update(inner_states)
|
|
133
|
+
events_definitions.update(inner_events)
|
|
134
|
+
|
|
135
|
+
if inner_history_defs:
|
|
136
|
+
inner_history, inner_events = _parse_history(inner_history_defs)
|
|
137
|
+
|
|
138
|
+
top_level_history = [
|
|
139
|
+
state._set_id(state_id)
|
|
140
|
+
for state_id, state in inner_history.items()
|
|
141
|
+
if not state.parent
|
|
142
|
+
]
|
|
143
|
+
state_definition["history"] = top_level_history # type: ignore
|
|
144
|
+
states_instances.update(inner_history)
|
|
145
|
+
events_definitions.update(inner_events)
|
|
146
|
+
|
|
147
|
+
if transition_defs:
|
|
148
|
+
events_definitions[state_id] = transition_defs
|
|
149
|
+
|
|
150
|
+
state_definition = cast(BaseStateKwargs, state_definition)
|
|
151
|
+
states_instances[state_id] = State(**state_definition)
|
|
152
|
+
|
|
153
|
+
return (states_instances, events_definitions)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_machine_class_from_definition(
|
|
157
|
+
name: str, states: Mapping[str, "StateKwargs | StateDefinition"], **definition
|
|
158
|
+
) -> "type[StateChart]": # noqa: C901
|
|
159
|
+
"""Create a StateChart class dynamically from a dictionary definition.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
name: The class name for the generated state machine.
|
|
163
|
+
states: A mapping of state IDs to state definitions. Each state definition
|
|
164
|
+
can include ``initial``, ``final``, ``parallel``, ``name``, ``value``,
|
|
165
|
+
``enter``/``exit`` callbacks, ``donedata``, nested ``states``,
|
|
166
|
+
``history``, and transitions via ``on`` (event-triggered) or
|
|
167
|
+
``transitions`` (eventless).
|
|
168
|
+
**definition: Additional keyword arguments passed to the metaclass
|
|
169
|
+
(e.g., ``validate_final_reachability=False``).
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A new StateChart subclass configured with the given states and transitions.
|
|
173
|
+
|
|
174
|
+
Example:
|
|
175
|
+
|
|
176
|
+
>>> machine = create_machine_class_from_definition(
|
|
177
|
+
... "TrafficLightMachine",
|
|
178
|
+
... **{
|
|
179
|
+
... "states": {
|
|
180
|
+
... "green": {"initial": True, "on": {"change": [{"target": "yellow"}]}},
|
|
181
|
+
... "yellow": {"on": {"change": [{"target": "red"}]}},
|
|
182
|
+
... "red": {"on": {"change": [{"target": "green"}]}},
|
|
183
|
+
... },
|
|
184
|
+
... }
|
|
185
|
+
... )
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
states_instances, events_definitions = _parse_states(states)
|
|
189
|
+
|
|
190
|
+
events: dict[str, TransitionList] = {}
|
|
191
|
+
for state_id, state_events in events_definitions.items():
|
|
192
|
+
for event_name, transitions_data in state_events.items():
|
|
193
|
+
for transition_data in transitions_data:
|
|
194
|
+
source = states_instances[state_id]
|
|
195
|
+
|
|
196
|
+
target_state_id = transition_data["target"]
|
|
197
|
+
transition_event_name = transition_data.get("event")
|
|
198
|
+
if event_name is not None and transition_event_name is not None:
|
|
199
|
+
transition_event_name = f"{event_name} {transition_event_name}"
|
|
200
|
+
elif event_name is not None:
|
|
201
|
+
transition_event_name = event_name
|
|
202
|
+
|
|
203
|
+
transition_kwargs = {
|
|
204
|
+
"event": transition_event_name,
|
|
205
|
+
"internal": transition_data.get("internal"),
|
|
206
|
+
"initial": transition_data.get("initial"),
|
|
207
|
+
"validators": transition_data.get("validators"),
|
|
208
|
+
"cond": transition_data.get("cond"),
|
|
209
|
+
"unless": transition_data.get("unless"),
|
|
210
|
+
"on": transition_data.get("on"),
|
|
211
|
+
"before": transition_data.get("before"),
|
|
212
|
+
"after": transition_data.get("after"),
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
# Handle multi-target transitions (space-separated target IDs)
|
|
216
|
+
if target_state_id and isinstance(target_state_id, str) and " " in target_state_id:
|
|
217
|
+
target_ids = target_state_id.split()
|
|
218
|
+
targets = [states_instances[tid] for tid in target_ids]
|
|
219
|
+
t = Transition(source, target=targets, **transition_kwargs)
|
|
220
|
+
source.transitions.add_transitions(t)
|
|
221
|
+
transition = TransitionList([t])
|
|
222
|
+
else:
|
|
223
|
+
target = states_instances[target_state_id] if target_state_id else None
|
|
224
|
+
transition = source.to(target, **transition_kwargs)
|
|
225
|
+
|
|
226
|
+
if event_name in events:
|
|
227
|
+
events[event_name] |= transition
|
|
228
|
+
elif event_name is not None:
|
|
229
|
+
events[event_name] = transition
|
|
230
|
+
|
|
231
|
+
top_level_states = {
|
|
232
|
+
state_id: state for state_id, state in states_instances.items() if not state.parent
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
attrs_mapper = {**definition, **top_level_states, **events}
|
|
236
|
+
return StateMachineMetaclass(name, (StateChart,), attrs_mapper) # type: ignore[return-value]
|