python-statemachine 3.1.2__py3-none-any.whl → 3.2.1__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.
Files changed (60) hide show
  1. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.1.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +5 -11
  6. statemachine/configuration.py +5 -6
  7. statemachine/contrib/diagram/extract.py +26 -27
  8. statemachine/contrib/diagram/formatter.py +5 -7
  9. statemachine/contrib/diagram/model.py +9 -11
  10. statemachine/contrib/diagram/renderers/dot.py +20 -26
  11. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  12. statemachine/contrib/diagram/renderers/table.py +7 -9
  13. statemachine/contrib/weighted.py +7 -11
  14. statemachine/dispatcher.py +18 -13
  15. statemachine/engines/async_.py +20 -9
  16. statemachine/engines/base.py +26 -17
  17. statemachine/event.py +1 -2
  18. statemachine/exceptions.py +2 -2
  19. statemachine/factory.py +11 -15
  20. statemachine/graph.py +2 -2
  21. statemachine/invoke.py +12 -11
  22. statemachine/io/__init__.py +52 -225
  23. statemachine/io/actions.py +627 -0
  24. statemachine/io/builder.py +195 -0
  25. statemachine/io/class_factory.py +236 -0
  26. statemachine/io/evaluators.py +303 -0
  27. statemachine/io/interpreter.py +128 -0
  28. statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
  29. statemachine/io/json/__init__.py +1 -0
  30. statemachine/io/json/reader.py +27 -0
  31. statemachine/io/loader.py +175 -0
  32. statemachine/io/model.py +268 -0
  33. statemachine/io/native.py +402 -0
  34. statemachine/io/ports.py +83 -0
  35. statemachine/io/schemas/statechart.schema.json +258 -0
  36. statemachine/io/scxml/__init__.py +15 -0
  37. statemachine/io/scxml/processor.py +23 -253
  38. statemachine/io/scxml/{parser.py → reader.py} +88 -56
  39. statemachine/io/system_variables.py +199 -0
  40. statemachine/io/validation.py +44 -0
  41. statemachine/io/yaml/__init__.py +1 -0
  42. statemachine/io/yaml/reader.py +65 -0
  43. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  44. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/orderedset.py +15 -6
  48. statemachine/registry.py +1 -4
  49. statemachine/signature.py +2 -5
  50. statemachine/spec_parser.py +228 -45
  51. statemachine/state.py +7 -8
  52. statemachine/statemachine.py +18 -20
  53. statemachine/states.py +3 -5
  54. statemachine/transition.py +3 -4
  55. statemachine/transition_list.py +4 -5
  56. statemachine/transition_mixin.py +1 -1
  57. python_statemachine-3.1.2.dist-info/RECORD +0 -58
  58. statemachine/io/scxml/actions.py +0 -650
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,83 @@
1
+ """Format ports & adapters: the reader Protocol and the format registry.
2
+
3
+ A :class:`FormatReader` is the *port* every format adapter implements: it turns
4
+ raw text into the neutral IR (:class:`~statemachine.io.model.StateMachineDefinition`).
5
+ Each format also declares which file extensions it owns and which processor builds
6
+ its IR into a :class:`~statemachine.statemachine.StateChart` class.
7
+
8
+ Adapters register themselves with :func:`register_format` (see the ``scxml``,
9
+ ``json`` and ``yaml`` reader modules). The high-level :func:`statemachine.io.load`
10
+ facade uses :func:`detect_format` and :func:`get_format` to wire everything up.
11
+ """
12
+
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from typing import Protocol
18
+ from typing import runtime_checkable
19
+
20
+ from .model import StateMachineDefinition
21
+
22
+
23
+ @runtime_checkable
24
+ class FormatReader(Protocol):
25
+ """A format adapter: parses text of one format into the neutral IR."""
26
+
27
+ def read(
28
+ self, text: str, *, source_name: "str | None" = None
29
+ ) -> StateMachineDefinition: # pragma: no cover - structural Protocol
30
+ ...
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class FormatSpec:
35
+ """Describes a supported serialization format.
36
+
37
+ The runtime is the format-neutral :class:`~statemachine.io.interpreter.Interpreter`;
38
+ a format only needs to provide a reader (text -> neutral IR).
39
+
40
+ Args:
41
+ name: the canonical format name (e.g. ``"scxml"``, ``"json"``, ``"yaml"``).
42
+ extensions: file extensions (with leading dot) that map to this format.
43
+ reader_factory: builds a :class:`FormatReader` for this format.
44
+ """
45
+
46
+ name: str
47
+ extensions: "tuple[str, ...]"
48
+ reader_factory: "Callable[..., Any]"
49
+
50
+
51
+ _FORMATS: "dict[str, FormatSpec]" = {}
52
+ _EXTENSIONS: "dict[str, str]" = {}
53
+
54
+
55
+ def register_format(spec: FormatSpec) -> None:
56
+ """Register a format spec, indexing it by name and by each of its extensions."""
57
+ _FORMATS[spec.name] = spec
58
+ for ext in spec.extensions:
59
+ _EXTENSIONS[ext] = spec.name
60
+
61
+
62
+ def get_format(name: str) -> FormatSpec:
63
+ """Return the :class:`FormatSpec` registered under ``name``."""
64
+ try:
65
+ return _FORMATS[name]
66
+ except KeyError:
67
+ available = ", ".join(sorted(_FORMATS)) or "(none registered)"
68
+ raise ValueError(f"Unknown format: {name!r}. Available formats: {available}.") from None
69
+
70
+
71
+ def detect_format(path: Path, explicit: "str | None" = None) -> str:
72
+ """Resolve the format name from an explicit override or the file extension."""
73
+ if explicit is not None:
74
+ return explicit
75
+ ext = path.suffix.lower()
76
+ try:
77
+ return _EXTENSIONS[ext]
78
+ except KeyError:
79
+ known = ", ".join(sorted(_EXTENSIONS)) or "(none registered)"
80
+ raise ValueError(
81
+ f"Cannot detect format from extension {ext!r}. Known extensions: "
82
+ f"{known}. Pass an explicit format=... argument."
83
+ ) from None
@@ -0,0 +1,258 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://fgmacedo.github.io/python-statemachine/schemas/statechart/v1.json",
4
+ "title": "python-statemachine statechart",
5
+ "description": "Declarative statechart definition for python-statemachine (JSON/YAML native format).",
6
+ "type": "object",
7
+ "required": ["states"],
8
+ "properties": {
9
+ "name": { "type": "string" },
10
+ "description": { "type": "string" },
11
+ "datamodel": {
12
+ "oneOf": [
13
+ { "type": "object", "additionalProperties": true },
14
+ { "type": "array", "items": { "$ref": "#/$defs/dataItem" } }
15
+ ]
16
+ },
17
+ "states": {
18
+ "type": "object",
19
+ "minProperties": 1,
20
+ "additionalProperties": { "$ref": "#/$defs/state" }
21
+ }
22
+ },
23
+ "additionalProperties": false,
24
+ "$defs": {
25
+ "flag": {
26
+ "oneOf": [
27
+ { "type": "boolean" },
28
+ { "type": "string", "enum": ["true", "false", "yes", "no", "on", "off"] }
29
+ ]
30
+ },
31
+ "guard": {
32
+ "oneOf": [
33
+ { "type": "string" },
34
+ { "type": "array", "items": { "type": "string" } }
35
+ ]
36
+ },
37
+ "dataItem": {
38
+ "type": "object",
39
+ "required": ["id"],
40
+ "properties": {
41
+ "id": { "type": "string" },
42
+ "expr": { "type": "string" },
43
+ "content": {},
44
+ "src": { "type": "string" }
45
+ },
46
+ "additionalProperties": false
47
+ },
48
+ "param": {
49
+ "type": "object",
50
+ "required": ["name"],
51
+ "properties": {
52
+ "name": { "type": "string" },
53
+ "expr": { "type": "string" },
54
+ "location": { "type": "string" }
55
+ },
56
+ "additionalProperties": false
57
+ },
58
+ "structuredAction": {
59
+ "type": "object",
60
+ "minProperties": 1,
61
+ "maxProperties": 1,
62
+ "properties": {
63
+ "assign": {
64
+ "type": "object",
65
+ "required": ["location"],
66
+ "properties": {
67
+ "location": { "type": "string" },
68
+ "expr": { "type": "string" }
69
+ },
70
+ "additionalProperties": false
71
+ },
72
+ "raise": {
73
+ "oneOf": [
74
+ { "type": "string" },
75
+ {
76
+ "type": "object",
77
+ "required": ["event"],
78
+ "properties": { "event": { "type": "string" } },
79
+ "additionalProperties": false
80
+ }
81
+ ]
82
+ },
83
+ "log": {
84
+ "oneOf": [
85
+ { "type": "string" },
86
+ {
87
+ "type": "object",
88
+ "properties": {
89
+ "label": { "type": "string" },
90
+ "expr": { "type": "string" }
91
+ },
92
+ "additionalProperties": false
93
+ }
94
+ ]
95
+ },
96
+ "script": { "type": "string" },
97
+ "cancel": {
98
+ "type": "object",
99
+ "properties": {
100
+ "sendid": { "type": "string" },
101
+ "sendidexpr": { "type": "string" }
102
+ },
103
+ "additionalProperties": false
104
+ },
105
+ "send": {
106
+ "type": "object",
107
+ "properties": {
108
+ "event": { "type": "string" },
109
+ "eventexpr": { "type": "string" },
110
+ "target": { "type": "string" },
111
+ "type": { "type": "string" },
112
+ "id": { "type": "string" },
113
+ "idlocation": { "type": "string" },
114
+ "delay": { "type": "string" },
115
+ "delayexpr": { "type": "string" },
116
+ "namelist": { "type": "string" },
117
+ "params": { "type": "array", "items": { "$ref": "#/$defs/param" } },
118
+ "content": { "type": "string" }
119
+ },
120
+ "additionalProperties": false
121
+ },
122
+ "foreach": {
123
+ "type": "object",
124
+ "required": ["array", "item"],
125
+ "properties": {
126
+ "array": { "type": "string" },
127
+ "item": { "type": "string" },
128
+ "index": { "type": "string" },
129
+ "do": { "$ref": "#/$defs/structuredActions" }
130
+ },
131
+ "additionalProperties": false
132
+ },
133
+ "if": {
134
+ "type": "object",
135
+ "required": ["cond"],
136
+ "properties": {
137
+ "cond": { "type": "string" },
138
+ "then": { "$ref": "#/$defs/structuredActions" },
139
+ "elif": {
140
+ "type": "array",
141
+ "items": {
142
+ "type": "object",
143
+ "required": ["cond"],
144
+ "properties": {
145
+ "cond": { "type": "string" },
146
+ "then": { "$ref": "#/$defs/structuredActions" }
147
+ },
148
+ "additionalProperties": false
149
+ }
150
+ },
151
+ "else": { "$ref": "#/$defs/structuredActions" }
152
+ },
153
+ "additionalProperties": false
154
+ }
155
+ },
156
+ "additionalProperties": false
157
+ },
158
+ "action": {
159
+ "oneOf": [
160
+ { "type": "string" },
161
+ { "$ref": "#/$defs/structuredAction" }
162
+ ]
163
+ },
164
+ "actions": {
165
+ "oneOf": [
166
+ { "$ref": "#/$defs/action" },
167
+ { "type": "array", "items": { "$ref": "#/$defs/action" } }
168
+ ]
169
+ },
170
+ "structuredActions": {
171
+ "oneOf": [
172
+ { "$ref": "#/$defs/structuredAction" },
173
+ { "type": "array", "items": { "$ref": "#/$defs/structuredAction" } }
174
+ ]
175
+ },
176
+ "invoke": {
177
+ "type": "object",
178
+ "properties": {
179
+ "type": { "type": "string" },
180
+ "typeexpr": { "type": "string" },
181
+ "src": { "type": "string" },
182
+ "srcexpr": { "type": "string" },
183
+ "id": { "type": "string" },
184
+ "idlocation": { "type": "string" },
185
+ "autoforward": { "$ref": "#/$defs/flag" },
186
+ "namelist": { "type": "string" },
187
+ "params": { "type": "array", "items": { "$ref": "#/$defs/param" } },
188
+ "content": {
189
+ "oneOf": [
190
+ { "type": "string" },
191
+ { "$ref": "#" }
192
+ ]
193
+ },
194
+ "finalize": { "$ref": "#/$defs/structuredActions" }
195
+ },
196
+ "additionalProperties": false
197
+ },
198
+ "invokeList": {
199
+ "oneOf": [
200
+ { "$ref": "#/$defs/invoke" },
201
+ { "type": "array", "items": { "$ref": "#/$defs/invoke" } }
202
+ ]
203
+ },
204
+ "transition": {
205
+ "type": "object",
206
+ "properties": {
207
+ "event": { "type": "string" },
208
+ "target": { "type": "string" },
209
+ "cond": { "$ref": "#/$defs/guard" },
210
+ "unless": { "$ref": "#/$defs/guard" },
211
+ "internal": { "$ref": "#/$defs/flag" },
212
+ "initial": { "$ref": "#/$defs/flag" },
213
+ "on": { "$ref": "#/$defs/actions" },
214
+ "before": { "$ref": "#/$defs/actions" },
215
+ "after": { "$ref": "#/$defs/actions" }
216
+ },
217
+ "additionalProperties": false
218
+ },
219
+ "history": {
220
+ "type": "object",
221
+ "properties": {
222
+ "type": { "type": "string", "enum": ["shallow", "deep"] },
223
+ "transitions": { "type": "array", "items": { "$ref": "#/$defs/transition" } }
224
+ },
225
+ "additionalProperties": false
226
+ },
227
+ "donedata": {
228
+ "type": "object",
229
+ "properties": {
230
+ "params": { "type": "array", "items": { "$ref": "#/$defs/param" } },
231
+ "content": { "type": "string" }
232
+ },
233
+ "additionalProperties": false
234
+ },
235
+ "state": {
236
+ "type": "object",
237
+ "properties": {
238
+ "initial": { "$ref": "#/$defs/flag" },
239
+ "final": { "$ref": "#/$defs/flag" },
240
+ "parallel": { "$ref": "#/$defs/flag" },
241
+ "enter": { "$ref": "#/$defs/actions" },
242
+ "exit": { "$ref": "#/$defs/actions" },
243
+ "transitions": { "type": "array", "items": { "$ref": "#/$defs/transition" } },
244
+ "invoke": { "$ref": "#/$defs/invokeList" },
245
+ "states": {
246
+ "type": "object",
247
+ "additionalProperties": { "$ref": "#/$defs/state" }
248
+ },
249
+ "history": {
250
+ "type": "object",
251
+ "additionalProperties": { "$ref": "#/$defs/history" }
252
+ },
253
+ "donedata": { "$ref": "#/$defs/donedata" }
254
+ },
255
+ "additionalProperties": false
256
+ }
257
+ }
258
+ }
@@ -0,0 +1,15 @@
1
+ """SCXML ingestion support.
2
+
3
+ SCXML is, per the W3C specification, *executable content*: ``cond``/``expr`` attributes and
4
+ ``<script>`` elements are evaluated in the document's datamodel language. This implementation
5
+ provides a Python datamodel and loads SCXML through the same
6
+ :func:`~statemachine.io.load` facade as the native JSON/YAML formats.
7
+
8
+ Like every format, SCXML is loaded **secure by default** (``trusted=False`` rejects
9
+ ``<script>`` and external ``<data src>`` / ``<invoke src>`` file references, evaluates
10
+ expressions with the restricted AST-allowlist evaluator, confines write targets to public model
11
+ attributes and magnitude-caps ``**``/``*``); pass ``trusted=True`` only for documents you
12
+ control. The parser also refuses ``<!DOCTYPE>``/DTD to block XML entity-expansion bombs. See the
13
+ security note in :mod:`statemachine.io` and the GHSA-v4jc-pm6r-3vj8, GHSA-fj3w-533r-fvf6,
14
+ GHSA-v3qq-3xvg-m77g, GHSA-4857-ggqc-p3jc and GHSA-r8gj-366q-cgvj advisories.
15
+ """
@@ -1,263 +1,33 @@
1
- import os
2
- from contextlib import contextmanager
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- from typing import Any
6
- from typing import Dict
7
- from typing import List
1
+ """Back-compatible SCXML entry point: a preconfigured neutral Interpreter.
8
2
 
9
- from ...event import Event
10
- from ...exceptions import InvalidDefinition
11
- from ...statemachine import StateChart
12
- from .. import HistoryDefinition
13
- from .. import StateDefinition
14
- from .. import TransitionDict
15
- from .. import TransitionsList
16
- from .. import create_machine_class_from_definition
17
- from .actions import Cond
18
- from .actions import DoneDataCallable
19
- from .actions import EventDataWrapper
20
- from .actions import ExecuteBlock
21
- from .actions import create_datamodel_action_callable
22
- from .actions import create_invoke_init_callable
23
- from .invoke import SCXMLInvoker
24
- from .parser import parse_scxml
25
- from .schema import HistoryState
26
- from .schema import InvokeDefinition
27
- from .schema import State
28
- from .schema import Transition
3
+ The runtime is the format-neutral :class:`~statemachine.io.interpreter.Interpreter`.
4
+ ``SCXMLProcessor`` is simply that interpreter wired with the SCXML reader and the
5
+ trusted/restricted evaluator, plus the ``parse_scxml`` convenience for parsing a document
6
+ from a string. New code should prefer :func:`statemachine.io.load` (which also reads files
7
+ and detects the format from the extension).
8
+ """
29
9
 
10
+ from ..evaluators import evaluator_for
11
+ from ..interpreter import Interpreter
12
+ from .reader import SCXMLReader
13
+ from .reader import parse_scxml
30
14
 
31
- @contextmanager
32
- def temporary_directory(new_current_dir):
33
- original_dir = os.getcwd()
34
- try:
35
- os.chdir(new_current_dir)
36
- yield
37
- finally:
38
- os.chdir(original_dir)
39
15
 
16
+ class SCXMLProcessor(Interpreter):
17
+ """Parses SCXML documents into :class:`~statemachine.statemachine.StateChart` classes.
40
18
 
41
- class IOProcessor:
42
- def __init__(self, processor: "SCXMLProcessor", machine: StateChart):
43
- self.scxml_processor = processor
44
- self.machine = machine
19
+ Args:
20
+ trusted: when ``False`` (default), datamodel expressions are evaluated by
21
+ a restricted AST-allowlist evaluator and ``<script>`` is rejected, so
22
+ loading a document cannot execute arbitrary code. When ``True``,
23
+ expressions and ``<script>`` are evaluated as arbitrary Python via
24
+ ``eval``/``exec`` — only use it for SCXML you trust (see the package
25
+ docstring and GHSA-v4jc-pm6r-3vj8).
26
+ """
45
27
 
46
- def __getitem__(self, name: str):
47
- return self
48
-
49
- @property
50
- def location(self):
51
- return self.machine.name
52
-
53
- def get(self, name: str):
54
- return getattr(self, name)
55
-
56
-
57
- @dataclass
58
- class SessionData:
59
- machine: StateChart
60
- processor: IOProcessor
61
- first_event_raised: bool = False
62
-
63
- def __post_init__(self):
64
- self.session_id = f"{self.machine.name}:{id(self.machine)}"
65
-
66
-
67
- class SCXMLProcessor:
68
- def __init__(self):
69
- self.scs: "Dict[str, type[StateChart]]" = {}
70
- self.sessions: Dict[str, SessionData] = {}
71
- self._ioprocessors = {
72
- "http://www.w3.org/TR/scxml/#SCXMLEventProcessor": self,
73
- "scxml": self,
74
- }
75
-
76
- def parse_scxml_file(self, path: Path):
77
- scxml_content = path.read_text()
78
- with temporary_directory(path.parent):
79
- return self.parse_scxml(path.stem, scxml_content)
28
+ def __init__(self, trusted: bool = False):
29
+ super().__init__(reader=SCXMLReader(), evaluator=evaluator_for(trusted))
80
30
 
81
31
  def parse_scxml(self, sm_name: str, scxml_content: str):
82
32
  definition = parse_scxml(scxml_content)
83
33
  self.process_definition(definition, location=definition.name or sm_name)
84
-
85
- def process_definition(self, definition, location: str, is_invoked: bool = False):
86
- states_dict = self._process_states(definition.states)
87
-
88
- # Find the initial state for inserting init callbacks
89
- try:
90
- initial_state = next(s for s in iter(states_dict.values()) if s.get("initial"))
91
- except StopIteration:
92
- initial_state = next(iter(states_dict.values()))
93
-
94
- if "enter" not in initial_state:
95
- initial_state["enter"] = []
96
-
97
- insert_pos = 0
98
-
99
- # For invoked children, insert invoke_init to pop _invoke_session/_invoke_params
100
- # from kwargs and store them on the machine instance before any other callbacks.
101
- if is_invoked:
102
- initial_state["enter"].insert(0, create_invoke_init_callable()) # type: ignore[union-attr]
103
- insert_pos = 1
104
-
105
- # Process datamodel (initial variables)
106
- if definition.datamodel:
107
- datamodel = create_datamodel_action_callable(definition.datamodel)
108
- if datamodel: # pragma: no branch – parse_datamodel guarantees non-empty
109
- if isinstance( # pragma: no branch – always a list from lines above
110
- initial_state["enter"], list
111
- ):
112
- initial_state["enter"].insert(insert_pos, datamodel) # type: ignore[arg-type]
113
-
114
- self._add(
115
- location,
116
- {
117
- "states": states_dict,
118
- "prepare_event": self._prepare_event,
119
- "validate_disconnected_states": False,
120
- "validate_trap_states": False,
121
- "validate_final_reachability": False,
122
- "start_configuration_values": list(definition.initial_states),
123
- },
124
- )
125
-
126
- def _prepare_event(self, *args, event: Event, **kwargs):
127
- machine = kwargs["machine"]
128
- session_data = self._get_session(machine)
129
-
130
- if not session_data.first_event_raised and event and event != "__initial__":
131
- session_data.first_event_raised = True
132
-
133
- _event: "EventDataWrapper | None" = None
134
- if session_data.first_event_raised:
135
- _event = EventDataWrapper(kwargs["event_data"])
136
-
137
- return {
138
- "_name": machine.name,
139
- "_sessionid": session_data.session_id,
140
- "_ioprocessors": session_data.processor,
141
- "_event": _event,
142
- }
143
-
144
- def _get_session(self, machine: StateChart):
145
- if machine.name not in self.sessions:
146
- self.sessions[machine.name] = SessionData(
147
- processor=IOProcessor(self, machine=machine), machine=machine
148
- )
149
- return self.sessions[machine.name]
150
-
151
- def _process_history(self, history: Dict[str, HistoryState]) -> Dict[str, HistoryDefinition]:
152
- states_dict: Dict[str, HistoryDefinition] = {}
153
- for state_id, state in history.items():
154
- state_dict = HistoryDefinition()
155
-
156
- state_dict["type"] = state.type
157
-
158
- # Process transitions
159
- if state.transitions:
160
- state_dict["transitions"] = self._process_transitions(state.transitions)
161
-
162
- states_dict[state_id] = state_dict
163
-
164
- return states_dict
165
-
166
- def _process_states(self, states: Dict[str, State]) -> Dict[str, StateDefinition]:
167
- states_dict: Dict[str, StateDefinition] = {}
168
- for state_id, state in states.items():
169
- states_dict[state_id] = self._process_state(state)
170
- return states_dict
171
-
172
- def _process_state(self, state: State) -> StateDefinition: # noqa: C901
173
- state_dict = StateDefinition()
174
- if state.initial:
175
- state_dict["initial"] = True
176
- if state.final:
177
- state_dict["final"] = True
178
- if state.parallel:
179
- state_dict["parallel"] = True
180
-
181
- # Process enter actions
182
- enter_callables: list = [
183
- ExecuteBlock(content) for content in state.onentry if not content.is_empty
184
- ]
185
- if enter_callables:
186
- state_dict["enter"] = enter_callables
187
- if state.final and state.donedata:
188
- state_dict["donedata"] = DoneDataCallable(state.donedata)
189
-
190
- # Process exit actions
191
- if state.onexit:
192
- callables = [ExecuteBlock(content) for content in state.onexit if not content.is_empty]
193
- state_dict["exit"] = callables
194
-
195
- # Process transitions
196
- if state.transitions:
197
- state_dict["transitions"] = self._process_transitions(state.transitions)
198
-
199
- # Process invoke elements
200
- if state.invocations:
201
- invokers = [self._process_invocation(inv) for inv in state.invocations]
202
- state_dict["invoke"] = invokers # type: ignore[typeddict-unknown-key]
203
-
204
- if state.states:
205
- state_dict["states"] = self._process_states(state.states)
206
-
207
- if state.history:
208
- state_dict["history"] = self._process_history(state.history)
209
-
210
- return state_dict
211
-
212
- def _process_invocation(self, invoke_def: InvokeDefinition) -> SCXMLInvoker:
213
- """Convert an InvokeDefinition into an SCXMLInvoker."""
214
- return SCXMLInvoker(
215
- definition=invoke_def,
216
- base_dir=os.getcwd(),
217
- register_child=self._register_child,
218
- )
219
-
220
- def _register_child(self, scxml_content: str, child_name: str) -> type:
221
- """Parse SCXML content, register it as a child machine, and return its class."""
222
- definition = parse_scxml(scxml_content)
223
- self.process_definition(definition, location=child_name, is_invoked=True)
224
- return self.scs[child_name]
225
-
226
- def _process_transitions(self, transitions: List[Transition]):
227
- result: TransitionsList = []
228
- for transition in transitions:
229
- event = transition.event or None
230
- transition_dict: TransitionDict = {
231
- "event": event,
232
- "target": transition.target,
233
- "internal": transition.internal,
234
- "initial": transition.initial,
235
- }
236
-
237
- # Process cond
238
- if transition.cond:
239
- cond_callable = Cond.create(transition.cond, processor=self)
240
- if cond_callable is not None: # pragma: no branch – cond already truthy
241
- transition_dict["cond"] = cond_callable
242
-
243
- # Process actions
244
- if transition.on and not transition.on.is_empty:
245
- transition_dict["on"] = ExecuteBlock(transition.on)
246
-
247
- result.append(transition_dict)
248
- return result
249
-
250
- def _add(self, location: str, definition: Dict[str, Any]):
251
- try:
252
- sc_class = create_machine_class_from_definition(location, **definition)
253
- self.scs[location] = sc_class
254
- return sc_class
255
- except Exception as e: # pragma: no cover
256
- raise InvalidDefinition(
257
- f"Failed to create state machine class: {e} from definition: {definition}"
258
- ) from e
259
-
260
- def start(self, **kwargs):
261
- self.root_cls = next(iter(self.scs.values()))
262
- self.root = self.root_cls(**kwargs)
263
- return self.root