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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Evaluation strategies for statechart datamodel expressions and scripts.
|
|
2
|
+
|
|
3
|
+
An :class:`Evaluator` turns the raw expression/script strings carried by the
|
|
4
|
+
neutral IR (:mod:`statemachine.io.model`) into callables, deciding *how* they are
|
|
5
|
+
evaluated:
|
|
6
|
+
|
|
7
|
+
- :class:`RestrictedEvaluator` (secure default): compiles expressions with the
|
|
8
|
+
AST-allowlist evaluator (:func:`statemachine.spec_parser.parse_expr`), which
|
|
9
|
+
cannot reach builtins, dunder attributes, or arbitrary calls. ``<script>`` and
|
|
10
|
+
raw-Python evaluation are rejected because they are arbitrary code.
|
|
11
|
+
- :class:`PythonEvaluator` (opt-in, ``trusted=True``): preserves the legacy
|
|
12
|
+
behavior of evaluating expressions and scripts as arbitrary Python via
|
|
13
|
+
``eval``/``exec``.
|
|
14
|
+
|
|
15
|
+
The restricted evaluator validates the *structure* of an expression at compile
|
|
16
|
+
time (a violation raises :class:`~statemachine.exceptions.InvalidDefinition`),
|
|
17
|
+
but name resolution and value computation still happen at call time, so runtime
|
|
18
|
+
errors (``NameError``, ``TypeError``) keep flowing to ``error.execution`` as
|
|
19
|
+
before.
|
|
20
|
+
|
|
21
|
+
The evaluator is also the trust gate for external resource references: an external
|
|
22
|
+
``<data src>`` / ``<invoke src>`` reads a local file, so the restricted evaluator rejects
|
|
23
|
+
it via :meth:`ensure_external_src_allowed` (the trusted one allows it).
|
|
24
|
+
|
|
25
|
+
This module is format-neutral: it is shared by the SCXML, JSON and YAML readers.
|
|
26
|
+
See the :mod:`statemachine.io.scxml` package docstring and the GHSA-v4jc-pm6r-3vj8
|
|
27
|
+
(code execution) and GHSA-fj3w-533r-fvf6 (file disclosure) advisories for the rationale.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import ast
|
|
31
|
+
import html
|
|
32
|
+
import re
|
|
33
|
+
from collections.abc import Callable
|
|
34
|
+
from inspect import isawaitable
|
|
35
|
+
from typing import Any
|
|
36
|
+
from typing import Protocol
|
|
37
|
+
|
|
38
|
+
from ..dispatcher import callable_method
|
|
39
|
+
from ..event import _event_data_kwargs
|
|
40
|
+
from ..exceptions import InvalidDefinition
|
|
41
|
+
from ..spec_parser import InState
|
|
42
|
+
from ..spec_parser import parse_expr
|
|
43
|
+
|
|
44
|
+
#: Attributes that must never be exposed to or overwritten by datamodel
|
|
45
|
+
#: expressions (engine internals and SCXML system variables).
|
|
46
|
+
protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_COND_REPLACEMENTS = {
|
|
50
|
+
"true": "True",
|
|
51
|
+
"false": "False",
|
|
52
|
+
"null": "None",
|
|
53
|
+
"===": "==",
|
|
54
|
+
"!==": "!=",
|
|
55
|
+
"&&": "and",
|
|
56
|
+
"||": "or",
|
|
57
|
+
}
|
|
58
|
+
_COND_PATTERN = re.compile(r"\b(?:true|false|null)\b|===|!==|&&|\|\|")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def normalize_cond(cond: str) -> str:
|
|
62
|
+
"""Normalize a JavaScript-like condition to Python syntax.
|
|
63
|
+
|
|
64
|
+
Decodes XML entities (e.g. ``<``) and maps ``true``/``false``/``null`` and
|
|
65
|
+
the ``===``/``!==``/``&&``/``||`` operators to their Python equivalents. This
|
|
66
|
+
is primarily an SCXML (ECMAScript datamodel) concern; for Python-style
|
|
67
|
+
expressions (JSON/YAML) it is effectively a no-op.
|
|
68
|
+
"""
|
|
69
|
+
cond = html.unescape(cond)
|
|
70
|
+
return _COND_PATTERN.sub(lambda match: _COND_REPLACEMENTS[match.group(0)], cond)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _resolvable_model_attr(name: str, model) -> bool:
|
|
74
|
+
"""The secure boundary for resolving a bare name off the model.
|
|
75
|
+
|
|
76
|
+
Engine-protected names (``machine``, ``event``, system variables, …) and any
|
|
77
|
+
private/dunder name (leading underscore) never resolve off the model, mirroring the
|
|
78
|
+
dotted-access guard in :func:`statemachine.spec_parser.build_attribute`. System
|
|
79
|
+
variables stay reachable because they are provided via the call kwargs, which are
|
|
80
|
+
consulted before this. This keeps a semi-trusted document from reading the model's
|
|
81
|
+
internals (``_secret``) or walking the object graph (``__class__``).
|
|
82
|
+
"""
|
|
83
|
+
return name not in protected_attrs and not name.startswith("_") and hasattr(model, name)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def variable_hook(name: str) -> Callable:
|
|
87
|
+
"""Resolve a datamodel name at call time.
|
|
88
|
+
|
|
89
|
+
Looks the name up in the call kwargs first (SCXML system variables such as
|
|
90
|
+
``_event``, ``_name``), then falls back to a (non-protected, non-private) attribute of
|
|
91
|
+
``machine.model``. Names in :data:`~statemachine.event._event_data_kwargs` (``machine``,
|
|
92
|
+
``model``, ``event``, ``transition``, ...) are the live engine-capability objects; they
|
|
93
|
+
are **withheld** from the kwargs lookup so an untrusted expression cannot obtain the
|
|
94
|
+
engine and pivot through it onto shared classes (GHSA-v3qq-3xvg-m77g). The ``_``-prefixed
|
|
95
|
+
system variables are not engine objects and stay resolvable.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def resolver(*args, **kwargs):
|
|
99
|
+
if name not in _event_data_kwargs and name in kwargs:
|
|
100
|
+
return kwargs[name]
|
|
101
|
+
model = kwargs["machine"].model
|
|
102
|
+
if _resolvable_model_attr(name, model):
|
|
103
|
+
return getattr(model, name)
|
|
104
|
+
raise NameError(f"name '{name}' is not defined")
|
|
105
|
+
|
|
106
|
+
resolver.__name__ = name
|
|
107
|
+
return resolver
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cond_variable_hook(name: str) -> Callable:
|
|
111
|
+
"""Resolve a name inside a guard, following the Python guard dialect.
|
|
112
|
+
|
|
113
|
+
Same lookup as :func:`variable_hook`, but a referenced **method** is *called*
|
|
114
|
+
(with dependency injection), while a property or plain attribute is *read*.
|
|
115
|
+
This keeps native ``cond``/``unless`` at parity with class-defined guards (see
|
|
116
|
+
``docs/guards.md``): a name can be a property, an attribute or a method.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def resolver(*args, **kwargs):
|
|
120
|
+
if name not in _event_data_kwargs and name in kwargs:
|
|
121
|
+
return kwargs[name]
|
|
122
|
+
model = kwargs["machine"].model
|
|
123
|
+
if _resolvable_model_attr(name, model):
|
|
124
|
+
value = getattr(model, name)
|
|
125
|
+
if callable(value):
|
|
126
|
+
return callable_method(value)(*args, **kwargs)
|
|
127
|
+
return value
|
|
128
|
+
raise NameError(f"name '{name}' is not defined")
|
|
129
|
+
|
|
130
|
+
resolver.__name__ = name
|
|
131
|
+
return resolver
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _eval(expr: str, **kwargs) -> Any:
|
|
135
|
+
"""Evaluate an expression as arbitrary Python (``trusted=True`` path only).
|
|
136
|
+
|
|
137
|
+
.. warning::
|
|
138
|
+
|
|
139
|
+
Calls the built-in :func:`eval` with no sandboxing. Only reachable when
|
|
140
|
+
the document is loaded with ``trusted=True``.
|
|
141
|
+
"""
|
|
142
|
+
if "machine" in kwargs:
|
|
143
|
+
kwargs.update(
|
|
144
|
+
**{
|
|
145
|
+
k: v
|
|
146
|
+
for k, v in kwargs["machine"].model.__dict__.items()
|
|
147
|
+
if k not in protected_attrs
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
kwargs["In"] = InState(kwargs["machine"])
|
|
151
|
+
return eval(expr, {}, kwargs)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Evaluator(Protocol):
|
|
155
|
+
"""Port: turns expression/script strings into callables.
|
|
156
|
+
|
|
157
|
+
This is the contract consumers (the processors, action compilers) depend on. They
|
|
158
|
+
never name a concrete implementation; wiring goes through :func:`evaluator_for`.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def compile_value(self, expr: str) -> Callable: # pragma: no cover - structural Protocol
|
|
162
|
+
"""Compile a value expression (``<assign>``, ``<send>``, ``<data>``, ...)."""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def compile_bool(self, expr: str) -> Callable: # pragma: no cover - structural Protocol
|
|
166
|
+
"""Compile a boolean guard expression (``cond``)."""
|
|
167
|
+
...
|
|
168
|
+
|
|
169
|
+
def compile_script(self, content: str) -> Callable: # pragma: no cover - structural Protocol
|
|
170
|
+
"""Compile a ``<script>`` body into a callable side effect."""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
def eval_literal(self, content: str) -> Any: # pragma: no cover - structural Protocol
|
|
174
|
+
"""Evaluate inline ``<content>`` as a literal, falling back to the raw string."""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
def ensure_external_src_allowed( # pragma: no cover - structural Protocol
|
|
178
|
+
self, descriptor: str
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Guard external resource references (``<data src>`` / ``<invoke src>``).
|
|
181
|
+
|
|
182
|
+
Reading a local file named by an untrusted document is a confidentiality risk, so
|
|
183
|
+
it is gated on the trust level just like ``<script>``. ``descriptor`` names the
|
|
184
|
+
offending element for the error message.
|
|
185
|
+
"""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class RestrictedEvaluator:
|
|
190
|
+
"""Secure default: AST-allowlist evaluation, no ``eval``/``exec``.
|
|
191
|
+
|
|
192
|
+
``<script>`` and any expression outside the supported subset (method calls,
|
|
193
|
+
dunder attribute access, lambdas, comprehensions, ...) are rejected at
|
|
194
|
+
compile time with :class:`~statemachine.exceptions.InvalidDefinition`.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
def compile_value(self, expr: str) -> Callable:
|
|
198
|
+
return self._compile(expr, variable_hook)
|
|
199
|
+
|
|
200
|
+
def compile_bool(self, expr: str) -> Callable:
|
|
201
|
+
# Guards resolve names with the Python dialect (methods are called); the
|
|
202
|
+
# result is coerced to bool, awaiting it first when a coroutine (an async
|
|
203
|
+
# guard method) flows through, so the async engine works too.
|
|
204
|
+
value_fn = self._compile(normalize_cond(expr), cond_variable_hook)
|
|
205
|
+
|
|
206
|
+
def cond(*args, **kwargs):
|
|
207
|
+
result = value_fn(*args, **kwargs)
|
|
208
|
+
if isawaitable(result):
|
|
209
|
+
|
|
210
|
+
async def _coerce():
|
|
211
|
+
return bool(await result)
|
|
212
|
+
|
|
213
|
+
return _coerce()
|
|
214
|
+
return bool(result)
|
|
215
|
+
|
|
216
|
+
return cond
|
|
217
|
+
|
|
218
|
+
def compile_script(self, content: str) -> Callable:
|
|
219
|
+
raise InvalidDefinition(
|
|
220
|
+
"<script> executes arbitrary code and is disabled by default. Pass "
|
|
221
|
+
"trusted=True to enable it, and only for trusted sources."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
def eval_literal(self, content: str) -> Any:
|
|
225
|
+
try:
|
|
226
|
+
return ast.literal_eval(content)
|
|
227
|
+
except (ValueError, SyntaxError):
|
|
228
|
+
return content
|
|
229
|
+
|
|
230
|
+
def ensure_external_src_allowed(self, descriptor: str) -> None:
|
|
231
|
+
raise InvalidDefinition(
|
|
232
|
+
f"{descriptor} loads a local file and is disabled by default. Pass "
|
|
233
|
+
"trusted=True to enable it, and only for trusted sources."
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _compile(expr: str, hook: Callable) -> Callable:
|
|
238
|
+
try:
|
|
239
|
+
return parse_expr(expr, hook)
|
|
240
|
+
except (ValueError, SyntaxError) as exc:
|
|
241
|
+
raise InvalidDefinition(
|
|
242
|
+
f"Expression {expr!r} is not allowed by the restricted "
|
|
243
|
+
f"datamodel ({exc}). Pass trusted=True to evaluate it as "
|
|
244
|
+
f"Python, and only for trusted sources."
|
|
245
|
+
) from exc
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class PythonEvaluator:
|
|
249
|
+
"""Opt-in (``trusted=True``): evaluate expressions and scripts as Python.
|
|
250
|
+
|
|
251
|
+
Preserves the legacy ``eval``/``exec`` behavior, including error timing
|
|
252
|
+
(syntax/name errors surface at call time and become ``error.execution``).
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def compile_value(self, expr: str) -> Callable:
|
|
256
|
+
def value(*args, **kwargs):
|
|
257
|
+
return _eval(expr, **kwargs)
|
|
258
|
+
|
|
259
|
+
return value
|
|
260
|
+
|
|
261
|
+
def compile_bool(self, expr: str) -> Callable:
|
|
262
|
+
# Intentionally uses ``eval`` (not ``parse_expr``): the whole point of the
|
|
263
|
+
# trusted evaluator is to accept guards the restricted AST allowlist rejects —
|
|
264
|
+
# method/function calls, builtins, etc. (e.g. SCXML conformance conds like
|
|
265
|
+
# ``_event.data.get('x') == 1`` or ``hasattr(_event, 'name')``). Routing this
|
|
266
|
+
# through ``parse_expr`` would make trusted mode no more capable than the
|
|
267
|
+
# restricted one for guards and would break those documents.
|
|
268
|
+
normalized = normalize_cond(expr)
|
|
269
|
+
|
|
270
|
+
def cond(*args, **kwargs):
|
|
271
|
+
return _eval(normalized, **kwargs)
|
|
272
|
+
|
|
273
|
+
return cond
|
|
274
|
+
|
|
275
|
+
def compile_script(self, content: str) -> Callable:
|
|
276
|
+
def script(*args, **kwargs):
|
|
277
|
+
machine = kwargs["machine"]
|
|
278
|
+
local_vars = {**machine.model.__dict__}
|
|
279
|
+
exec(content, {}, local_vars)
|
|
280
|
+
for var_name, value in local_vars.items():
|
|
281
|
+
setattr(machine.model, var_name, value)
|
|
282
|
+
|
|
283
|
+
return script
|
|
284
|
+
|
|
285
|
+
def eval_literal(self, content: str) -> Any:
|
|
286
|
+
try:
|
|
287
|
+
return eval(content, {}, {})
|
|
288
|
+
except (NameError, SyntaxError, TypeError):
|
|
289
|
+
return content
|
|
290
|
+
|
|
291
|
+
def ensure_external_src_allowed(self, descriptor: str) -> None:
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def evaluator_for(trusted: bool = False) -> Evaluator:
|
|
296
|
+
"""Return the evaluation strategy for the given trust level (the wiring point).
|
|
297
|
+
|
|
298
|
+
This is the single place that maps the public ``trusted`` flag to a concrete
|
|
299
|
+
adapter, so no other module needs to import :class:`RestrictedEvaluator` or
|
|
300
|
+
:class:`PythonEvaluator`. The default (``trusted=False``) is the secure restricted
|
|
301
|
+
evaluator, so callers that omit the argument get the safe behaviour.
|
|
302
|
+
"""
|
|
303
|
+
return PythonEvaluator() if trusted else RestrictedEvaluator()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Format-neutral runtime that turns statechart definitions into running machines.
|
|
2
|
+
|
|
3
|
+
The :class:`Interpreter` is the execution-model runtime, independent of input syntax. It
|
|
4
|
+
owns a :class:`~statemachine.io.builder.DefinitionBuilder` (compilation), keeps the
|
|
5
|
+
registry of compiled classes (``scs``, needed for ``<invoke>``), manages per-machine
|
|
6
|
+
sessions, injects the system variables (``_event``/``_sessionid``/``_name``/
|
|
7
|
+
``_ioprocessors``) on every event, coordinates invoke, and instantiates the root machine.
|
|
8
|
+
|
|
9
|
+
It is parameterized by two ports: a :class:`~statemachine.io.ports.FormatReader` (so invoke
|
|
10
|
+
can compile children in the same format) and an
|
|
11
|
+
:class:`~statemachine.io.evaluators.Evaluator` (secure by default). SCXML is just one
|
|
12
|
+
reader; YAML/JSON get the very same runtime behavior.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ..exceptions import InvalidDefinition
|
|
19
|
+
from ..statemachine import StateChart
|
|
20
|
+
from .builder import DefinitionBuilder
|
|
21
|
+
from .class_factory import create_machine_class_from_definition
|
|
22
|
+
from .evaluators import Evaluator
|
|
23
|
+
from .invoke import Invoker
|
|
24
|
+
from .model import InvokeDefinition
|
|
25
|
+
from .model import StateMachineDefinition
|
|
26
|
+
from .system_variables import IOProcessor
|
|
27
|
+
from .system_variables import SessionData
|
|
28
|
+
from .system_variables import build_system_variables
|
|
29
|
+
from .system_variables import create_invoke_init_callable
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Interpreter:
|
|
33
|
+
"""Runtime that compiles and hosts statecharts from any format.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
reader: the format reader, used to compile invoked children in the same format.
|
|
37
|
+
evaluator: the evaluation strategy for guards and executable content.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, *, reader, evaluator: Evaluator):
|
|
41
|
+
self.reader = reader
|
|
42
|
+
self._evaluator = evaluator
|
|
43
|
+
self._builder = DefinitionBuilder(evaluator=evaluator, hooks=self)
|
|
44
|
+
self.scs: "dict[str, type[StateChart]]" = {}
|
|
45
|
+
self.sessions: dict[str, SessionData] = {}
|
|
46
|
+
|
|
47
|
+
def process_definition(
|
|
48
|
+
self, definition: StateMachineDefinition, location: str, is_invoked: bool = False
|
|
49
|
+
):
|
|
50
|
+
kwargs = self._builder.build_class_kwargs(definition, is_invoked=is_invoked)
|
|
51
|
+
self._add(location, kwargs)
|
|
52
|
+
|
|
53
|
+
def start(self, **kwargs):
|
|
54
|
+
self.root_cls = next(iter(self.scs.values()))
|
|
55
|
+
self.root = self.root_cls(**kwargs)
|
|
56
|
+
return self.root
|
|
57
|
+
|
|
58
|
+
# -- RuntimeHooks (called back by the DefinitionBuilder) -----------------------
|
|
59
|
+
|
|
60
|
+
def initial_enter_prefix(self, is_invoked: bool) -> list:
|
|
61
|
+
# Invoked children store _invoke_session/_invoke_params before any other callback.
|
|
62
|
+
if is_invoked:
|
|
63
|
+
return [create_invoke_init_callable()]
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
def definition_kwargs(self, definition) -> "dict[str, Any]":
|
|
67
|
+
"""Extra keyword arguments passed to ``create_machine_class_from_definition``.
|
|
68
|
+
|
|
69
|
+
The three structural ``validate_*`` checks are turned **off** for loaded
|
|
70
|
+
statecharts: declarative documents legitimately express configurations these
|
|
71
|
+
checks would reject (states reached only through parallel regions or eventless
|
|
72
|
+
paths, intentional trap/error states). The trade-off is that genuine structural
|
|
73
|
+
inconsistencies are not caught at load time; see the validations reference and
|
|
74
|
+
``docs/io``. ``prepare_event`` injects the system variables for every format.
|
|
75
|
+
"""
|
|
76
|
+
return {
|
|
77
|
+
"validate_disconnected_states": False,
|
|
78
|
+
"validate_trap_states": False,
|
|
79
|
+
"validate_final_reachability": False,
|
|
80
|
+
"start_configuration_values": list(definition.initial_states),
|
|
81
|
+
"prepare_event": self._prepare_event,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def make_invoker(self, invoke_def: InvokeDefinition) -> Invoker:
|
|
85
|
+
return Invoker(
|
|
86
|
+
definition=invoke_def,
|
|
87
|
+
base_dir=os.getcwd(),
|
|
88
|
+
register_child=self._register_child,
|
|
89
|
+
evaluator=self._evaluator,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# -- Runtime services ----------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def _prepare_event(self, *args, event, **kwargs):
|
|
95
|
+
machine = kwargs["machine"]
|
|
96
|
+
session_data = self._get_session(machine)
|
|
97
|
+
return build_system_variables(machine, session_data, event, kwargs["event_data"])
|
|
98
|
+
|
|
99
|
+
def _get_session(self, machine) -> SessionData:
|
|
100
|
+
if machine.name not in self.sessions:
|
|
101
|
+
self.sessions[machine.name] = SessionData(
|
|
102
|
+
processor=IOProcessor(self, machine=machine), machine=machine
|
|
103
|
+
)
|
|
104
|
+
return self.sessions[machine.name]
|
|
105
|
+
|
|
106
|
+
def _register_child(self, content, child_name: str) -> type:
|
|
107
|
+
"""Compile and register a child statechart.
|
|
108
|
+
|
|
109
|
+
``content`` is either source text (parsed via this interpreter's reader, so the
|
|
110
|
+
child is in the same format) or an already-parsed
|
|
111
|
+
:class:`~statemachine.io.model.StateMachineDefinition` (native inline child).
|
|
112
|
+
"""
|
|
113
|
+
if isinstance(content, StateMachineDefinition):
|
|
114
|
+
definition = content
|
|
115
|
+
else:
|
|
116
|
+
definition = self.reader.read(content)
|
|
117
|
+
self.process_definition(definition, location=child_name, is_invoked=True)
|
|
118
|
+
return self.scs[child_name]
|
|
119
|
+
|
|
120
|
+
def _add(self, location: str, definition: dict[str, Any]):
|
|
121
|
+
try:
|
|
122
|
+
sc_class = create_machine_class_from_definition(location, **definition)
|
|
123
|
+
self.scs[location] = sc_class
|
|
124
|
+
return sc_class
|
|
125
|
+
except Exception as e: # pragma: no cover
|
|
126
|
+
raise InvalidDefinition(
|
|
127
|
+
f"Failed to create state machine class: {e} from definition: {definition}"
|
|
128
|
+
) from e
|