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.
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
- python_statemachine-3.2.1.dist-info/RECORD +72 -0
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/extract.py +26 -27
- 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 +18 -13
- statemachine/engines/async_.py +20 -9
- statemachine/engines/base.py +26 -17
- statemachine/event.py +1 -2
- statemachine/exceptions.py +2 -2
- statemachine/factory.py +11 -15
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +52 -225
- statemachine/io/actions.py +627 -0
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +303 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +175 -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 +15 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +88 -56
- statemachine/io/system_variables.py +199 -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 +19 -19
- statemachine/orderedset.py +15 -6
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +228 -45
- statemachine/state.py +7 -8
- 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.2.dist-info/RECORD +0 -58
- statemachine/io/scxml/actions.py +0 -650
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
statemachine/io/ports.py
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
47
|
-
|
|
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
|