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
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Format-neutral invoke handler.
|
|
2
2
|
|
|
3
|
-
Implements the IInvoke protocol
|
|
4
|
-
|
|
5
|
-
the child machine lifecycle including ``#_parent`` routing,
|
|
6
|
-
finalize.
|
|
3
|
+
Implements the engine's ``IInvoke`` protocol: resolves a child statechart (inline
|
|
4
|
+
content, ``src`` file, or ``srcexpr``), evaluates ``params``/``namelist`` in the parent
|
|
5
|
+
context, and manages the child machine lifecycle including ``#_parent`` routing,
|
|
6
|
+
autoforward and finalize.
|
|
7
|
+
|
|
8
|
+
The child is compiled via the ``register_child`` callback (which the interpreter wires to
|
|
9
|
+
its own format reader), so invoke is not tied to SCXML: a child may be authored in any
|
|
10
|
+
format the interpreter understands.
|
|
7
11
|
"""
|
|
8
12
|
|
|
9
13
|
import asyncio
|
|
10
14
|
import logging
|
|
15
|
+
from collections.abc import Callable
|
|
11
16
|
from inspect import isawaitable
|
|
12
17
|
from pathlib import Path
|
|
13
18
|
from typing import Any
|
|
14
|
-
from typing import Callable
|
|
15
19
|
|
|
16
|
-
from
|
|
17
|
-
from
|
|
20
|
+
from ..invoke import IInvoke
|
|
21
|
+
from ..invoke import InvokeContext
|
|
18
22
|
from .actions import ExecuteBlock
|
|
19
|
-
from .actions import
|
|
20
|
-
from .
|
|
23
|
+
from .actions import _ensure_writable_target
|
|
24
|
+
from .evaluators import Evaluator
|
|
25
|
+
from .model import InvokeDefinition
|
|
26
|
+
from .model import StateMachineDefinition
|
|
27
|
+
from .system_variables import EventDataWrapper
|
|
21
28
|
|
|
22
29
|
logger = logging.getLogger(__name__)
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
#: Invoke ``type`` values understood by the runtime (SCXML processor URLs + ``None``).
|
|
32
|
+
VALID_INVOKE_TYPES = {
|
|
25
33
|
None,
|
|
26
34
|
"scxml",
|
|
27
35
|
"http://www.w3.org/TR/scxml",
|
|
@@ -30,10 +38,10 @@ _VALID_INVOKE_TYPES = {
|
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
|
|
33
|
-
class
|
|
34
|
-
"""
|
|
41
|
+
class Invoker:
|
|
42
|
+
"""Invoke handler implementing the IInvoke protocol.
|
|
35
43
|
|
|
36
|
-
Resolves the child
|
|
44
|
+
Resolves the child statechart from inline content, a ``src`` file or ``srcexpr``,
|
|
37
45
|
evaluates params/namelist, and manages the child machine lifecycle.
|
|
38
46
|
"""
|
|
39
47
|
|
|
@@ -41,12 +49,20 @@ class SCXMLInvoker:
|
|
|
41
49
|
self,
|
|
42
50
|
definition: InvokeDefinition,
|
|
43
51
|
base_dir: str,
|
|
44
|
-
register_child: "Callable[[str, str], type]",
|
|
52
|
+
register_child: "Callable[[StateMachineDefinition | str, str], type]",
|
|
53
|
+
evaluator: Evaluator,
|
|
45
54
|
):
|
|
46
55
|
self._definition = definition
|
|
47
56
|
self._register_child = register_child
|
|
48
57
|
self._child: Any = None
|
|
49
58
|
self._base_dir: str = base_dir
|
|
59
|
+
self._evaluator: Evaluator = evaluator
|
|
60
|
+
|
|
61
|
+
# A static ``src`` is known at load time, so reject it now (like <data src> /
|
|
62
|
+
# <script>) for a clean, propagating error. A dynamic ``srcexpr`` can only be
|
|
63
|
+
# resolved at runtime and is gated again in ``_resolve_content``.
|
|
64
|
+
if definition.content is None and definition.src is not None:
|
|
65
|
+
self._evaluator.ensure_external_src_allowed("<invoke src=...>")
|
|
50
66
|
|
|
51
67
|
# Duck-typed attributes for InvokeManager
|
|
52
68
|
self.invoke_id: "str | None" = definition.id
|
|
@@ -56,7 +72,10 @@ class SCXMLInvoker:
|
|
|
56
72
|
# Pre-compile finalize block
|
|
57
73
|
self._finalize_block: "ExecuteBlock | None" = None
|
|
58
74
|
if definition.finalize and not definition.finalize.is_empty:
|
|
59
|
-
self._finalize_block = ExecuteBlock(definition.finalize)
|
|
75
|
+
self._finalize_block = ExecuteBlock(definition.finalize, self._evaluator)
|
|
76
|
+
|
|
77
|
+
def _eval(self, expr: str, machine) -> Any:
|
|
78
|
+
return self._evaluator.compile_value(expr)(machine=machine)
|
|
60
79
|
|
|
61
80
|
def run(self, ctx: InvokeContext) -> Any:
|
|
62
81
|
"""Create and run the child state machine."""
|
|
@@ -64,32 +83,33 @@ class SCXMLInvoker:
|
|
|
64
83
|
|
|
65
84
|
# Store invokeid in idlocation if specified
|
|
66
85
|
if self.idlocation:
|
|
86
|
+
_ensure_writable_target(self.idlocation, "<invoke> 'idlocation'")
|
|
67
87
|
setattr(machine.model, self.idlocation, ctx.invokeid)
|
|
68
88
|
|
|
69
89
|
# Resolve invoke type
|
|
70
90
|
invoke_type = self._definition.type
|
|
71
91
|
if self._definition.typeexpr:
|
|
72
|
-
invoke_type = _eval(self._definition.typeexpr, machine=machine)
|
|
92
|
+
invoke_type = self._eval(self._definition.typeexpr, machine=machine)
|
|
73
93
|
|
|
74
|
-
if invoke_type not in
|
|
94
|
+
if invoke_type not in VALID_INVOKE_TYPES:
|
|
75
95
|
raise ValueError(
|
|
76
|
-
f"Unsupported invoke type: {invoke_type}. Supported types: {
|
|
96
|
+
f"Unsupported invoke type: {invoke_type}. Supported types: {VALID_INVOKE_TYPES}"
|
|
77
97
|
)
|
|
78
98
|
|
|
79
|
-
# Resolve child
|
|
80
|
-
|
|
81
|
-
if
|
|
99
|
+
# Resolve child statechart content
|
|
100
|
+
child_content = self._resolve_content(machine)
|
|
101
|
+
if child_content is None:
|
|
82
102
|
raise ValueError("No content resolved for <invoke>")
|
|
83
103
|
|
|
84
104
|
# Evaluate params and namelist
|
|
85
105
|
invoke_params = self._evaluate_params(machine)
|
|
86
106
|
|
|
87
107
|
# Parse and create the child machine
|
|
88
|
-
child_cls = self._create_child_class(
|
|
108
|
+
child_cls = self._create_child_class(child_content, ctx.invokeid)
|
|
89
109
|
|
|
90
110
|
# _invoke_session and _invoke_params are passed as kwargs so that the
|
|
91
111
|
# invoke_init callback (inserted at position 0 in the initial state's onentry
|
|
92
|
-
# by the
|
|
112
|
+
# by the interpreter) can pop them and store them on the machine instance.
|
|
93
113
|
#
|
|
94
114
|
# The _ChildRefSetter listener captures ``self._child`` during the first
|
|
95
115
|
# state entry, before the processing loop blocks. This is necessary
|
|
@@ -108,7 +128,7 @@ class SCXMLInvoker:
|
|
|
108
128
|
|
|
109
129
|
def on_cancel(self):
|
|
110
130
|
"""Cancel the child machine and all its invocations."""
|
|
111
|
-
from
|
|
131
|
+
from ..invoke import _stop_child_machine
|
|
112
132
|
|
|
113
133
|
_stop_child_machine(self._child)
|
|
114
134
|
self._child = None
|
|
@@ -129,49 +149,65 @@ class SCXMLInvoker:
|
|
|
129
149
|
"machine": machine,
|
|
130
150
|
"model": machine.model,
|
|
131
151
|
}
|
|
132
|
-
# Inject SCXML context variables
|
|
133
|
-
from .actions import EventDataWrapper
|
|
134
|
-
|
|
135
152
|
kwargs.update(
|
|
136
153
|
{k: v for k, v in machine.model.__dict__.items() if not k.startswith("_")}
|
|
137
154
|
)
|
|
138
|
-
# Build EventDataWrapper from trigger_data's kwargs
|
|
139
155
|
kwargs["_event"] = EventDataWrapper.from_trigger_data(trigger_data)
|
|
140
156
|
self._finalize_block(**kwargs)
|
|
141
157
|
|
|
142
|
-
def _resolve_content(self, machine)
|
|
143
|
-
"""Resolve the child
|
|
158
|
+
def _resolve_content(self, machine):
|
|
159
|
+
"""Resolve the child statechart content from content/src/srcexpr.
|
|
160
|
+
|
|
161
|
+
Returns either a source string (file text / inline document text) that the reader
|
|
162
|
+
will parse, or an already-parsed definition (native inline child), which the
|
|
163
|
+
interpreter registers directly.
|
|
164
|
+
"""
|
|
144
165
|
defn = self._definition
|
|
145
166
|
|
|
146
|
-
if defn.content:
|
|
147
|
-
|
|
148
|
-
if
|
|
149
|
-
|
|
167
|
+
if defn.content is not None:
|
|
168
|
+
content = defn.content
|
|
169
|
+
if not isinstance(content, str):
|
|
170
|
+
# Native inline child: an already-parsed StateMachineDefinition.
|
|
171
|
+
return content
|
|
172
|
+
if self._is_inline_document(content):
|
|
173
|
+
return content
|
|
150
174
|
# It's an expression — evaluate it
|
|
151
|
-
result = _eval(
|
|
175
|
+
result = self._eval(content, machine=machine)
|
|
152
176
|
if isinstance(result, str):
|
|
153
177
|
return result
|
|
154
178
|
return str(result)
|
|
155
179
|
|
|
156
180
|
if defn.srcexpr:
|
|
157
|
-
src = _eval(defn.srcexpr, machine=machine)
|
|
181
|
+
src = self._eval(defn.srcexpr, machine=machine)
|
|
158
182
|
elif defn.src:
|
|
159
183
|
src = defn.src
|
|
160
184
|
else:
|
|
161
185
|
return None
|
|
162
186
|
|
|
187
|
+
# Reading a local file named by an untrusted document is a confidentiality risk,
|
|
188
|
+
# so it is gated on the trust level (mirrors <script> / <data src>).
|
|
189
|
+
self._evaluator.ensure_external_src_allowed("<invoke src=...>")
|
|
190
|
+
|
|
163
191
|
# Handle file: URIs and relative paths
|
|
164
192
|
if src.startswith("file:"):
|
|
165
193
|
path = Path(src.removeprefix("file:"))
|
|
166
194
|
else:
|
|
167
195
|
path = Path(src)
|
|
168
196
|
|
|
169
|
-
# Resolve relative to the base directory of the parent
|
|
197
|
+
# Resolve relative to the base directory of the parent document
|
|
170
198
|
if not path.is_absolute():
|
|
171
199
|
path = Path(self._base_dir) / path
|
|
172
200
|
|
|
173
201
|
return path.read_text()
|
|
174
202
|
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _is_inline_document(content: str) -> bool:
|
|
205
|
+
"""Whether ``content`` is an inline document (vs an expression to evaluate).
|
|
206
|
+
|
|
207
|
+
The default heuristic recognizes inline XML (SCXML). Other formats may override.
|
|
208
|
+
"""
|
|
209
|
+
return content.lstrip().startswith("<")
|
|
210
|
+
|
|
175
211
|
def _evaluate_params(self, machine) -> dict:
|
|
176
212
|
"""Evaluate params and namelist into a dict of values."""
|
|
177
213
|
defn = self._definition
|
|
@@ -186,28 +222,28 @@ class SCXMLInvoker:
|
|
|
186
222
|
# Evaluate param elements
|
|
187
223
|
for param in defn.params:
|
|
188
224
|
if param.expr is not None:
|
|
189
|
-
result[param.name] = _eval(param.expr, machine=machine)
|
|
225
|
+
result[param.name] = self._eval(param.expr, machine=machine)
|
|
190
226
|
elif param.location is not None:
|
|
191
|
-
result[param.name] = _eval(param.location, machine=machine)
|
|
227
|
+
result[param.name] = self._eval(param.location, machine=machine)
|
|
192
228
|
|
|
193
229
|
return result
|
|
194
230
|
|
|
195
|
-
def _create_child_class(self,
|
|
196
|
-
"""
|
|
231
|
+
def _create_child_class(self, content: "StateMachineDefinition | str", invokeid: str):
|
|
232
|
+
"""Compile the child statechart and create a machine class."""
|
|
197
233
|
child_name = f"invoke_{invokeid}"
|
|
198
|
-
return self._register_child(
|
|
234
|
+
return self._register_child(content, child_name)
|
|
199
235
|
|
|
200
236
|
|
|
201
237
|
class _ChildRefSetter:
|
|
202
238
|
"""Listener that captures the child machine reference during initialization.
|
|
203
239
|
|
|
204
240
|
The child's ``__init__`` blocks inside the processing loop (e.g. when there
|
|
205
|
-
are delayed events). By using this listener, ``
|
|
206
|
-
|
|
207
|
-
|
|
241
|
+
are delayed events). By using this listener, ``Invoker._child`` is set during
|
|
242
|
+
the first state entry — *before* the processing loop starts spinning — so that
|
|
243
|
+
``on_event()`` can forward events to the child immediately.
|
|
208
244
|
"""
|
|
209
245
|
|
|
210
|
-
def __init__(self, invoker: "
|
|
246
|
+
def __init__(self, invoker: "Invoker"):
|
|
211
247
|
self._invoker = invoker
|
|
212
248
|
|
|
213
249
|
def on_enter_state(self, machine=None, **kwargs):
|
|
@@ -221,13 +257,17 @@ class _InvokeSession:
|
|
|
221
257
|
def __init__(self, parent, invokeid: str):
|
|
222
258
|
self.parent = parent
|
|
223
259
|
self.invokeid = invokeid
|
|
260
|
+
self._pending_tasks: "set[asyncio.Future]" = set()
|
|
261
|
+
"""Strong refs to scheduled sends; the loop only holds weak refs to tasks."""
|
|
224
262
|
|
|
225
263
|
def send_to_parent(self, event: str, **data):
|
|
226
264
|
"""Send an event to the parent machine's external queue."""
|
|
227
265
|
result = self.parent.send(event, _invokeid=self.invokeid, **data)
|
|
228
266
|
if isawaitable(result):
|
|
229
|
-
asyncio.ensure_future(result)
|
|
267
|
+
task = asyncio.ensure_future(result)
|
|
268
|
+
self._pending_tasks.add(task)
|
|
269
|
+
task.add_done_callback(self._pending_tasks.discard)
|
|
230
270
|
|
|
231
271
|
|
|
232
272
|
# Verify protocol compliance at import time
|
|
233
|
-
assert isinstance(
|
|
273
|
+
assert isinstance(Invoker.__new__(Invoker), IInvoke)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""JSON statechart format adapter."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""JSON format adapter: parse a JSON statechart document into the neutral IR."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from ..model import StateMachineDefinition
|
|
6
|
+
from ..native import native_dict_to_definition
|
|
7
|
+
from ..ports import FormatSpec
|
|
8
|
+
from ..ports import register_format
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JSONReader:
|
|
12
|
+
"""Format adapter that parses JSON documents (stdlib :mod:`json`) into the IR."""
|
|
13
|
+
|
|
14
|
+
def parse_document(self, text: str) -> dict:
|
|
15
|
+
return json.loads(text) # type: ignore[no-any-return]
|
|
16
|
+
|
|
17
|
+
def read(self, text: str, *, source_name: "str | None" = None) -> StateMachineDefinition:
|
|
18
|
+
return native_dict_to_definition(self.parse_document(text), source_name=source_name)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
register_format(
|
|
22
|
+
FormatSpec(
|
|
23
|
+
name="json",
|
|
24
|
+
extensions=(".json",),
|
|
25
|
+
reader_factory=JSONReader,
|
|
26
|
+
)
|
|
27
|
+
)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""High-level, format-neutral facade for loading statecharts.
|
|
2
|
+
|
|
3
|
+
:func:`load` is the simple entry point: give it a file path (format detected by
|
|
4
|
+
extension) or inline content (with an explicit ``format=``) and it returns the
|
|
5
|
+
ready-to-instantiate :class:`~statemachine.statemachine.StateChart` class. It is
|
|
6
|
+
**secure by default** — expressions are evaluated by a restricted AST-allowlist
|
|
7
|
+
evaluator and ``<script>`` / arbitrary Python is rejected unless ``trusted=True``.
|
|
8
|
+
|
|
9
|
+
For advanced scenarios (a document declaring several statecharts, or SCXML files
|
|
10
|
+
that import/invoke others), use :func:`build_processor` to get the underlying
|
|
11
|
+
processor and reach ``.scs`` / ``.start()``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import cast
|
|
18
|
+
|
|
19
|
+
from ..statemachine import StateChart
|
|
20
|
+
from .evaluators import evaluator_for
|
|
21
|
+
from .interpreter import Interpreter
|
|
22
|
+
from .json import reader as _json_reader # noqa: F401
|
|
23
|
+
from .ports import detect_format
|
|
24
|
+
from .ports import get_format
|
|
25
|
+
|
|
26
|
+
# Importing the reader modules registers their formats (extensions). None of these
|
|
27
|
+
# imports pulls in PyYAML or jsonschema at module load time.
|
|
28
|
+
from .scxml import reader as _scxml_reader # noqa: F401
|
|
29
|
+
from .yaml import reader as _yaml_reader # noqa: F401
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@contextmanager
|
|
33
|
+
def _chdir(new_dir: Path):
|
|
34
|
+
original = os.getcwd()
|
|
35
|
+
try:
|
|
36
|
+
os.chdir(new_dir)
|
|
37
|
+
yield
|
|
38
|
+
finally:
|
|
39
|
+
os.chdir(original)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_existing_file(source: str) -> bool:
|
|
43
|
+
"""Whether ``source`` names an existing file, treating "too long"/invalid as not a file.
|
|
44
|
+
|
|
45
|
+
A long single-line inline document (e.g. minified JSON) is not a path, but reaches
|
|
46
|
+
:meth:`Path.is_file`, which on some platforms raises ``OSError`` (``ENAMETOOLONG``) or
|
|
47
|
+
``ValueError`` (embedded NUL) instead of returning ``False``. Swallow those so inline
|
|
48
|
+
content is never mistaken for a crash; a real, resolvable path still returns ``True``.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
return Path(source).is_file()
|
|
52
|
+
except (OSError, ValueError):
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve_source(source: "str | Path", format: "str | None"):
|
|
57
|
+
"""Return ``(text, format_name, location_hint, base_dir)`` for a source.
|
|
58
|
+
|
|
59
|
+
A :class:`~pathlib.Path`, or a single-line string naming an existing file, is
|
|
60
|
+
read from disk (format detected by extension unless overridden). Any other
|
|
61
|
+
string is treated as inline content and requires an explicit ``format``.
|
|
62
|
+
"""
|
|
63
|
+
if isinstance(source, Path):
|
|
64
|
+
path: "Path | None" = source
|
|
65
|
+
elif "\n" not in source and _is_existing_file(source):
|
|
66
|
+
path = Path(source)
|
|
67
|
+
else:
|
|
68
|
+
path = None
|
|
69
|
+
|
|
70
|
+
if path is not None:
|
|
71
|
+
text = path.read_text()
|
|
72
|
+
fmt = detect_format(path, format)
|
|
73
|
+
return text, fmt, path.stem, path.parent
|
|
74
|
+
|
|
75
|
+
fmt = detect_format(Path("<inline>"), format)
|
|
76
|
+
return source, fmt, None, None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _build(source, *, format, trusted, validate, name):
|
|
80
|
+
"""Shared pipeline: read source -> IR -> interpreter (returns ``(interpreter, location)``)."""
|
|
81
|
+
text, fmt, location_hint, base_dir = _resolve_source(source, format)
|
|
82
|
+
spec = get_format(fmt)
|
|
83
|
+
reader = spec.reader_factory()
|
|
84
|
+
|
|
85
|
+
parse_document = getattr(reader, "parse_document", None)
|
|
86
|
+
if validate and parse_document is None:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"validate=True is not supported for the {fmt!r} format; "
|
|
89
|
+
"validation applies to the native JSON/YAML schema."
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _read_and_build():
|
|
93
|
+
# Parsing and compilation both run here, inside the file's directory when loading
|
|
94
|
+
# from a file, so a reader that resolves external references (e.g. SCXML
|
|
95
|
+
# ``<data src="...">``) and invoke ``src`` resolve relative to the document.
|
|
96
|
+
if parse_document is not None:
|
|
97
|
+
doc = parse_document(text)
|
|
98
|
+
if validate:
|
|
99
|
+
from .validation import validate_document
|
|
100
|
+
|
|
101
|
+
validate_document(doc)
|
|
102
|
+
from .native import native_dict_to_definition
|
|
103
|
+
|
|
104
|
+
definition = native_dict_to_definition(doc, source_name=name or location_hint)
|
|
105
|
+
else:
|
|
106
|
+
definition = reader.read(text, source_name=name or location_hint)
|
|
107
|
+
|
|
108
|
+
location = name or definition.name or location_hint or "statechart"
|
|
109
|
+
# The runtime is the format-neutral Interpreter, wired with this format's reader
|
|
110
|
+
# (so invoked children compile in the same format) and the chosen evaluator.
|
|
111
|
+
interpreter = Interpreter(reader=reader, evaluator=evaluator_for(trusted))
|
|
112
|
+
interpreter.process_definition(definition, location=location)
|
|
113
|
+
return interpreter, location
|
|
114
|
+
|
|
115
|
+
if base_dir is not None:
|
|
116
|
+
with _chdir(base_dir):
|
|
117
|
+
return _read_and_build()
|
|
118
|
+
return _read_and_build()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def build_processor(
|
|
122
|
+
source: "str | Path",
|
|
123
|
+
*,
|
|
124
|
+
format: "str | None" = None,
|
|
125
|
+
trusted: bool = False,
|
|
126
|
+
validate: bool = False,
|
|
127
|
+
name: "str | None" = None,
|
|
128
|
+
):
|
|
129
|
+
"""Load a statechart and return the underlying interpreter (low-level API).
|
|
130
|
+
|
|
131
|
+
Returns the :class:`~statemachine.io.interpreter.Interpreter`. Use it when you need
|
|
132
|
+
access to ``interpreter.scs`` (all compiled classes) or ``interpreter.start(...)`` —
|
|
133
|
+
e.g. documents that invoke/import children. See :func:`load` for the argument semantics.
|
|
134
|
+
"""
|
|
135
|
+
interpreter, _location = _build(
|
|
136
|
+
source, format=format, trusted=trusted, validate=validate, name=name
|
|
137
|
+
)
|
|
138
|
+
return interpreter
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def load(
|
|
142
|
+
source: "str | Path",
|
|
143
|
+
*,
|
|
144
|
+
format: "str | None" = None,
|
|
145
|
+
trusted: bool = False,
|
|
146
|
+
validate: bool = False,
|
|
147
|
+
name: "str | None" = None,
|
|
148
|
+
) -> "type[StateChart]":
|
|
149
|
+
"""Load a statechart from a file or inline content and return its class.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
source: a file path (``str``/:class:`~pathlib.Path`; format detected from
|
|
153
|
+
the extension) or inline document content (requires ``format``).
|
|
154
|
+
format: explicit format name (``"scxml"``, ``"json"``, ``"yaml"``),
|
|
155
|
+
overriding extension detection and required for inline content.
|
|
156
|
+
trusted: when ``False`` (default), expressions are evaluated by a restricted
|
|
157
|
+
AST-allowlist evaluator and ``<script>`` / arbitrary Python is rejected.
|
|
158
|
+
When ``True``, expressions and scripts are evaluated as arbitrary Python.
|
|
159
|
+
validate: when ``True`` (native JSON/YAML only), validate the document against
|
|
160
|
+
the published JSON Schema before building (requires the ``[validation]``
|
|
161
|
+
extra).
|
|
162
|
+
name: explicit name for the generated class (defaults to the document name
|
|
163
|
+
or the file stem).
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
The :class:`~statemachine.statemachine.StateChart` subclass. Instantiate it
|
|
167
|
+
to run the machine (``load("m.yaml")()``).
|
|
168
|
+
"""
|
|
169
|
+
interpreter, location = _build(
|
|
170
|
+
source, format=format, trusted=trusted, validate=validate, name=name
|
|
171
|
+
)
|
|
172
|
+
cls = interpreter.scs[location]
|
|
173
|
+
# Keep the interpreter reachable (and alive) from the returned class.
|
|
174
|
+
cls._io_processor = interpreter # pyright: ignore[reportAttributeAccessIssue]
|
|
175
|
+
return cast("type[StateChart]", cls)
|