python-statemachine 3.1.2__py3-none-any.whl → 3.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
- python_statemachine-3.2.0.dist-info/RECORD +72 -0
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.0.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/extract.py +23 -24
- statemachine/contrib/diagram/formatter.py +5 -7
- statemachine/contrib/diagram/model.py +9 -11
- statemachine/contrib/diagram/renderers/dot.py +20 -26
- statemachine/contrib/diagram/renderers/mermaid.py +36 -40
- statemachine/contrib/diagram/renderers/table.py +7 -9
- statemachine/contrib/weighted.py +7 -11
- statemachine/dispatcher.py +13 -12
- statemachine/engines/async_.py +5 -6
- statemachine/engines/base.py +12 -14
- statemachine/event.py +1 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +11 -15
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +45 -225
- statemachine/io/{scxml/actions.py → actions.py} +158 -288
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +275 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +161 -0
- statemachine/io/model.py +268 -0
- statemachine/io/native.py +402 -0
- statemachine/io/ports.py +83 -0
- statemachine/io/schemas/statechart.schema.json +258 -0
- statemachine/io/scxml/__init__.py +12 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +64 -47
- statemachine/io/system_variables.py +184 -0
- statemachine/io/validation.py +44 -0
- statemachine/io/yaml/__init__.py +1 -0
- statemachine/io/yaml/reader.py +65 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/orderedset.py +3 -3
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +171 -42
- statemachine/state.py +5 -6
- statemachine/statemachine.py +18 -20
- statemachine/states.py +3 -5
- statemachine/transition.py +3 -4
- statemachine/transition_list.py +4 -5
- statemachine/transition_mixin.py +1 -1
- python_statemachine-3.1.2.dist-info/RECORD +0 -58
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,31 +1,32 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import xml.etree.ElementTree as ET
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
4
|
from typing import Literal
|
|
5
|
-
from typing import Set
|
|
6
5
|
from typing import cast
|
|
7
6
|
from urllib.parse import urlparse
|
|
8
7
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
-
from
|
|
14
|
-
from
|
|
15
|
-
from
|
|
16
|
-
from
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
from
|
|
23
|
-
from
|
|
24
|
-
from
|
|
25
|
-
from
|
|
26
|
-
from
|
|
27
|
-
from
|
|
28
|
-
from
|
|
8
|
+
from ..model import Action
|
|
9
|
+
from ..model import AssignAction
|
|
10
|
+
from ..model import CancelAction
|
|
11
|
+
from ..model import DataItem
|
|
12
|
+
from ..model import DataModel
|
|
13
|
+
from ..model import DoneData
|
|
14
|
+
from ..model import ExecutableContent
|
|
15
|
+
from ..model import ForeachAction
|
|
16
|
+
from ..model import HistoryState
|
|
17
|
+
from ..model import IfAction
|
|
18
|
+
from ..model import IfBranch
|
|
19
|
+
from ..model import InvokeDefinition
|
|
20
|
+
from ..model import LogAction
|
|
21
|
+
from ..model import Param
|
|
22
|
+
from ..model import RaiseAction
|
|
23
|
+
from ..model import ScriptAction
|
|
24
|
+
from ..model import SendAction
|
|
25
|
+
from ..model import State
|
|
26
|
+
from ..model import StateMachineDefinition
|
|
27
|
+
from ..model import Transition
|
|
28
|
+
from ..ports import FormatSpec
|
|
29
|
+
from ..ports import register_format
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
def strip_namespaces(tree: ET.Element):
|
|
@@ -40,7 +41,7 @@ def strip_namespaces(tree: ET.Element):
|
|
|
40
41
|
attrib[new_name] = attrib.pop(name)
|
|
41
42
|
|
|
42
43
|
|
|
43
|
-
def _parse_initial(initial_content: "str | None") ->
|
|
44
|
+
def _parse_initial(initial_content: "str | None") -> list[str]:
|
|
44
45
|
if initial_content is None:
|
|
45
46
|
return []
|
|
46
47
|
return initial_content.split()
|
|
@@ -87,13 +88,13 @@ def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
|
|
|
87
88
|
return definition
|
|
88
89
|
|
|
89
90
|
|
|
90
|
-
def _find_own_datamodel_elements(root: ET.Element) ->
|
|
91
|
+
def _find_own_datamodel_elements(root: ET.Element) -> list[ET.Element]:
|
|
91
92
|
"""Find <datamodel> elements that belong to this SCXML document, not to inline children.
|
|
92
93
|
|
|
93
94
|
Skips any <datamodel> nested inside <content> elements (which contain inline
|
|
94
95
|
child SCXML documents for <invoke>).
|
|
95
96
|
"""
|
|
96
|
-
result:
|
|
97
|
+
result: list[ET.Element] = []
|
|
97
98
|
|
|
98
99
|
def _walk(elem: ET.Element):
|
|
99
100
|
for child in elem:
|
|
@@ -122,7 +123,7 @@ def parse_datamodel(root: ET.Element) -> "DataModel | None":
|
|
|
122
123
|
data_model.data.append(
|
|
123
124
|
DataItem(
|
|
124
125
|
id=data_elem.attrib["id"],
|
|
125
|
-
src=
|
|
126
|
+
src=src,
|
|
126
127
|
expr=data_elem.attrib.get("expr"),
|
|
127
128
|
content=content,
|
|
128
129
|
)
|
|
@@ -157,7 +158,7 @@ def parse_history(state_elem: ET.Element) -> HistoryState:
|
|
|
157
158
|
|
|
158
159
|
def parse_state( # noqa: C901
|
|
159
160
|
state_elem: ET.Element,
|
|
160
|
-
initial_states:
|
|
161
|
+
initial_states: set[str],
|
|
161
162
|
is_final: bool = False,
|
|
162
163
|
is_parallel: bool = False,
|
|
163
164
|
) -> State:
|
|
@@ -279,25 +280,11 @@ def parse_executable_content(element: ET.Element) -> ExecutableContent:
|
|
|
279
280
|
|
|
280
281
|
|
|
281
282
|
def parse_element(element: ET.Element) -> Action:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
elif tag == "log":
|
|
288
|
-
return parse_log(element)
|
|
289
|
-
elif tag == "if":
|
|
290
|
-
return parse_if(element)
|
|
291
|
-
elif tag == "send":
|
|
292
|
-
return parse_send(element)
|
|
293
|
-
elif tag == "script":
|
|
294
|
-
return parse_script(element)
|
|
295
|
-
elif tag == "foreach":
|
|
296
|
-
return parse_foreach(element)
|
|
297
|
-
elif tag == "cancel":
|
|
298
|
-
return parse_cancel(element)
|
|
299
|
-
|
|
300
|
-
raise ValueError(f"Unknown tag: {tag}")
|
|
283
|
+
try:
|
|
284
|
+
parser = _ELEMENT_PARSERS[element.tag]
|
|
285
|
+
except KeyError:
|
|
286
|
+
raise ValueError(f"Unknown tag: {element.tag}") from None
|
|
287
|
+
return parser(element)
|
|
301
288
|
|
|
302
289
|
|
|
303
290
|
def parse_raise(element: ET.Element) -> RaiseAction:
|
|
@@ -434,7 +421,7 @@ def parse_invoke(element: ET.Element) -> InvokeDefinition:
|
|
|
434
421
|
autoforward = element.attrib.get("autoforward", "false").lower() == "true"
|
|
435
422
|
namelist = element.attrib.get("namelist")
|
|
436
423
|
|
|
437
|
-
params:
|
|
424
|
+
params: list[Param] = []
|
|
438
425
|
content: "str | None" = None
|
|
439
426
|
finalize: "ExecutableContent | None" = None
|
|
440
427
|
|
|
@@ -471,3 +458,33 @@ def parse_invoke(element: ET.Element) -> InvokeDefinition:
|
|
|
471
458
|
content=content,
|
|
472
459
|
finalize=finalize,
|
|
473
460
|
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
#: Dispatch table for executable-content elements (all parsers share the same
|
|
464
|
+
#: ``(element) -> Action`` signature, so a lookup is cleaner than a branch chain).
|
|
465
|
+
_ELEMENT_PARSERS: "dict[str, Callable[[ET.Element], Action]]" = {
|
|
466
|
+
"raise": parse_raise,
|
|
467
|
+
"assign": parse_assign,
|
|
468
|
+
"log": parse_log,
|
|
469
|
+
"if": parse_if,
|
|
470
|
+
"send": parse_send,
|
|
471
|
+
"script": parse_script,
|
|
472
|
+
"foreach": parse_foreach,
|
|
473
|
+
"cancel": parse_cancel,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class SCXMLReader:
|
|
478
|
+
"""Format adapter that parses SCXML (XML) documents into the neutral IR."""
|
|
479
|
+
|
|
480
|
+
def read(self, text: str, *, source_name: "str | None" = None) -> StateMachineDefinition:
|
|
481
|
+
return parse_scxml(text)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
register_format(
|
|
485
|
+
FormatSpec(
|
|
486
|
+
name="scxml",
|
|
487
|
+
extensions=(".scxml", ".xml"),
|
|
488
|
+
reader_factory=SCXMLReader,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Runtime system variables of the statechart execution model.
|
|
2
|
+
|
|
3
|
+
These are part of the (SCXML-derived) execution model the library implements, not of
|
|
4
|
+
any particular syntax — so they are format-neutral and available to statecharts loaded
|
|
5
|
+
from SCXML, JSON or YAML alike. The :class:`~statemachine.io.interpreter.Interpreter`
|
|
6
|
+
injects them on every event via :func:`build_system_variables`:
|
|
7
|
+
|
|
8
|
+
- ``_event``: the current event, wrapped as :class:`EventDataWrapper`
|
|
9
|
+
(``name``/``data``/``type``/``origintype``/``sendid``/``invokeid``).
|
|
10
|
+
- ``_sessionid``: a stable id for the running machine session.
|
|
11
|
+
- ``_name``: the machine name.
|
|
12
|
+
- ``_ioprocessors``: the session's :class:`IOProcessor`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class _Data:
|
|
21
|
+
kwargs: dict
|
|
22
|
+
|
|
23
|
+
def __getattr__(self, name):
|
|
24
|
+
return self.kwargs.get(name, None)
|
|
25
|
+
|
|
26
|
+
def get(self, name, default=None):
|
|
27
|
+
return self.kwargs.get(name, default)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OriginTypeSCXML(str):
|
|
31
|
+
"""The origintype of an :ref:`Event` as specified by the SCXML namespace."""
|
|
32
|
+
|
|
33
|
+
def __eq__(self, other):
|
|
34
|
+
return other == "http://www.w3.org/TR/scxml/#SCXMLEventProcessor" or other == "scxml"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class EventDataWrapper:
|
|
38
|
+
"""The ``_event`` system variable: a read-only view of the current event.
|
|
39
|
+
|
|
40
|
+
Exposes ``name``/``data``/``type``/``origintype``/``sendid``/``invokeid`` following
|
|
41
|
+
the SCXML event model, which is the library's execution model regardless of the
|
|
42
|
+
source syntax.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
origin: str = ""
|
|
46
|
+
origintype: str = OriginTypeSCXML("scxml")
|
|
47
|
+
invokeid: str = ""
|
|
48
|
+
"""If this event is generated from an invoked child process, the Processor MUST set
|
|
49
|
+
this field to the invoke id of the invocation that triggered the child process.
|
|
50
|
+
Otherwise it MUST leave it blank.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, event_data=None, *, trigger_data=None):
|
|
54
|
+
self.event_data = event_data
|
|
55
|
+
if trigger_data is not None:
|
|
56
|
+
self.trigger_data = trigger_data
|
|
57
|
+
elif event_data is not None:
|
|
58
|
+
self.trigger_data = event_data.trigger_data
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError("Either event_data or trigger_data must be provided")
|
|
61
|
+
|
|
62
|
+
td = self.trigger_data
|
|
63
|
+
self.sendid = td.send_id
|
|
64
|
+
self.invokeid = td.kwargs.get("_invokeid", "")
|
|
65
|
+
if td.event is None or td.event.internal:
|
|
66
|
+
if "error.execution" == td.event:
|
|
67
|
+
self.type = "platform"
|
|
68
|
+
else:
|
|
69
|
+
self.type = "internal"
|
|
70
|
+
self.origintype = ""
|
|
71
|
+
else:
|
|
72
|
+
self.type = "external"
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_trigger_data(cls, trigger_data):
|
|
76
|
+
"""Create an EventDataWrapper directly from a TriggerData (no EventData needed)."""
|
|
77
|
+
return cls(trigger_data=trigger_data)
|
|
78
|
+
|
|
79
|
+
def __getattr__(self, name):
|
|
80
|
+
if self.event_data is not None:
|
|
81
|
+
return getattr(self.event_data, name)
|
|
82
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
83
|
+
|
|
84
|
+
def __eq__(self, value):
|
|
85
|
+
"This makes SCXML test 329 pass. It assumes that the event is the same instance"
|
|
86
|
+
return isinstance(value, EventDataWrapper)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def name(self):
|
|
90
|
+
if self.event_data is not None:
|
|
91
|
+
return self.event_data.event
|
|
92
|
+
return str(self.trigger_data.event) if self.trigger_data.event else None
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def data(self):
|
|
96
|
+
"Property used to access the event payload (the SCXML ``_event.data``)."
|
|
97
|
+
td = self.trigger_data
|
|
98
|
+
if td.kwargs:
|
|
99
|
+
return _Data(td.kwargs)
|
|
100
|
+
elif td.args and len(td.args) == 1:
|
|
101
|
+
return td.args[0]
|
|
102
|
+
elif td.args:
|
|
103
|
+
return td.args
|
|
104
|
+
else:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class IOProcessor:
|
|
109
|
+
"""The ``_ioprocessors`` system variable for a session.
|
|
110
|
+
|
|
111
|
+
A minimal Event I/O Processor handle: indexing by any processor name returns itself,
|
|
112
|
+
and ``location`` is the machine name.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(self, interpreter, machine):
|
|
116
|
+
self.interpreter = interpreter
|
|
117
|
+
self.machine = machine
|
|
118
|
+
|
|
119
|
+
def __getitem__(self, name: str):
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def location(self):
|
|
124
|
+
return self.machine.name
|
|
125
|
+
|
|
126
|
+
def get(self, name: str):
|
|
127
|
+
return getattr(self, name)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class SessionData:
|
|
132
|
+
"""Per-machine runtime session state held by the interpreter."""
|
|
133
|
+
|
|
134
|
+
machine: Any
|
|
135
|
+
processor: IOProcessor
|
|
136
|
+
first_event_raised: bool = False
|
|
137
|
+
|
|
138
|
+
def __post_init__(self):
|
|
139
|
+
self.session_id = f"{self.machine.name}:{id(self.machine)}"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def build_system_variables(machine, session_data: SessionData, event, event_data) -> dict:
|
|
143
|
+
"""Compute the system variables to inject into callbacks for the current event.
|
|
144
|
+
|
|
145
|
+
``_event`` is exposed only after the first real (non-``__initial__``) event, matching
|
|
146
|
+
the SCXML rule that ``_event`` is unbound during the initial macrostep.
|
|
147
|
+
"""
|
|
148
|
+
if not session_data.first_event_raised and event and event != "__initial__":
|
|
149
|
+
session_data.first_event_raised = True
|
|
150
|
+
|
|
151
|
+
_event: "EventDataWrapper | None" = None
|
|
152
|
+
if session_data.first_event_raised:
|
|
153
|
+
_event = EventDataWrapper(event_data)
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"_name": machine.name,
|
|
157
|
+
"_sessionid": session_data.session_id,
|
|
158
|
+
"_ioprocessors": session_data.processor,
|
|
159
|
+
"_event": _event,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_invoke_init_callable():
|
|
164
|
+
"""Create a callback that extracts invoke-specific kwargs and stores them on the machine.
|
|
165
|
+
|
|
166
|
+
Inserted at position 0 in the initial state's onentry list for invoked children, so
|
|
167
|
+
``_invoke_session`` and ``_invoke_params`` are handled before any other callbacks run,
|
|
168
|
+
even for machines without a datamodel.
|
|
169
|
+
"""
|
|
170
|
+
initialized = False
|
|
171
|
+
|
|
172
|
+
def invoke_init(*args, **kwargs):
|
|
173
|
+
nonlocal initialized
|
|
174
|
+
if initialized:
|
|
175
|
+
return
|
|
176
|
+
initialized = True
|
|
177
|
+
machine = kwargs.get("machine")
|
|
178
|
+
if machine is not None:
|
|
179
|
+
# Use get() not pop(): each callback receives a copy of kwargs
|
|
180
|
+
# (via EventData.extended_kwargs), so pop would be misleading.
|
|
181
|
+
machine._invoke_params = kwargs.get("_invoke_params")
|
|
182
|
+
machine._invoke_session = kwargs.get("_invoke_session")
|
|
183
|
+
|
|
184
|
+
return invoke_init
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Optional JSON Schema validation for the native (JSON/YAML) statechart format.
|
|
2
|
+
|
|
3
|
+
Requires the optional ``[validation]`` extra (``jsonschema``). The schema itself is
|
|
4
|
+
shipped with the package at ``statemachine/io/schemas/statechart.schema.json`` and is
|
|
5
|
+
also published so editors/tools can reference it via ``$schema``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from importlib.resources import files
|
|
11
|
+
|
|
12
|
+
from ..exceptions import InvalidDefinition
|
|
13
|
+
|
|
14
|
+
SCHEMA_RESOURCE = "statechart.schema.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@lru_cache(maxsize=1)
|
|
18
|
+
def load_schema() -> dict:
|
|
19
|
+
"""Load and cache the bundled statechart JSON Schema."""
|
|
20
|
+
resource = files("statemachine.io").joinpath("schemas").joinpath(SCHEMA_RESOURCE)
|
|
21
|
+
return json.loads(resource.read_text(encoding="utf-8")) # type: ignore[no-any-return]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_document(doc) -> None:
|
|
25
|
+
"""Validate a parsed native document against the statechart JSON Schema.
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
InvalidDefinition: if the document does not conform to the schema, or if the
|
|
29
|
+
optional ``jsonschema`` dependency is not installed.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
import jsonschema # type: ignore[import-untyped]
|
|
33
|
+
except ModuleNotFoundError as exc: # pragma: no cover - exercised via import mock
|
|
34
|
+
raise InvalidDefinition(
|
|
35
|
+
"validate=True requires the 'jsonschema' package. Install it with: "
|
|
36
|
+
'pip install "python-statemachine[validation]"'
|
|
37
|
+
) from exc
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
jsonschema.validate(instance=doc, schema=load_schema())
|
|
41
|
+
except jsonschema.ValidationError as exc:
|
|
42
|
+
raise InvalidDefinition(
|
|
43
|
+
f"Statechart document failed schema validation: {exc.message}"
|
|
44
|
+
) from exc
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""YAML statechart format adapter (requires the optional ``[yaml]`` extra)."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""YAML format adapter: parse a YAML statechart document into the neutral IR.
|
|
2
|
+
|
|
3
|
+
Requires the optional ``[yaml]`` extra (PyYAML). YAML is always parsed with
|
|
4
|
+
``yaml.safe_load`` semantics so a document can never instantiate arbitrary Python
|
|
5
|
+
objects.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ..model import StateMachineDefinition
|
|
9
|
+
from ..native import native_dict_to_definition
|
|
10
|
+
from ..ports import FormatSpec
|
|
11
|
+
from ..ports import register_format
|
|
12
|
+
|
|
13
|
+
_LOADER = None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_loader(yaml):
|
|
17
|
+
"""Build a SafeLoader that does NOT coerce ``on``/``off``/``yes``/``no`` to bool.
|
|
18
|
+
|
|
19
|
+
YAML 1.1 turns those tokens into booleans, which silently mangles state ids
|
|
20
|
+
like ``off``/``on`` into ``True``/``False``. We keep ``yaml.safe_load``'s safety
|
|
21
|
+
but restrict the implicit bool resolver to ``true``/``false`` only.
|
|
22
|
+
"""
|
|
23
|
+
global _LOADER
|
|
24
|
+
if _LOADER is not None:
|
|
25
|
+
return _LOADER
|
|
26
|
+
|
|
27
|
+
class _StatechartSafeLoader(yaml.SafeLoader):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
_StatechartSafeLoader.yaml_implicit_resolvers = {
|
|
31
|
+
first_char: [
|
|
32
|
+
(tag, regexp)
|
|
33
|
+
for (tag, regexp) in resolvers
|
|
34
|
+
if not (tag == "tag:yaml.org,2002:bool" and first_char not in "tTfF")
|
|
35
|
+
]
|
|
36
|
+
for first_char, resolvers in yaml.SafeLoader.yaml_implicit_resolvers.items()
|
|
37
|
+
}
|
|
38
|
+
_LOADER = _StatechartSafeLoader
|
|
39
|
+
return _LOADER
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class YAMLReader:
|
|
43
|
+
"""Format adapter that parses YAML documents (``yaml.safe_load``) into the IR."""
|
|
44
|
+
|
|
45
|
+
def parse_document(self, text: str) -> dict:
|
|
46
|
+
try:
|
|
47
|
+
import yaml # type: ignore[import-untyped]
|
|
48
|
+
except ModuleNotFoundError as exc: # pragma: no cover - exercised via import mock
|
|
49
|
+
raise ImportError(
|
|
50
|
+
"YAML support requires PyYAML. Install it with: "
|
|
51
|
+
'pip install "python-statemachine[yaml]"'
|
|
52
|
+
) from exc
|
|
53
|
+
return yaml.load(text, Loader=_make_loader(yaml)) # type: ignore[no-any-return]
|
|
54
|
+
|
|
55
|
+
def read(self, text: str, *, source_name: "str | None" = None) -> StateMachineDefinition:
|
|
56
|
+
return native_dict_to_definition(self.parse_document(text), source_name=source_name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
register_format(
|
|
60
|
+
FormatSpec(
|
|
61
|
+
name="yaml",
|
|
62
|
+
extensions=(".yaml", ".yml"),
|
|
63
|
+
reader_factory=YAMLReader,
|
|
64
|
+
)
|
|
65
|
+
)
|
|
@@ -5,7 +5,7 @@ msgid ""
|
|
|
5
5
|
msgstr ""
|
|
6
6
|
"Project-Id-Version: 3.1.1\n"
|
|
7
7
|
"Report-Msgid-Bugs-To: fgmacedo@gmail.com\n"
|
|
8
|
-
"POT-Creation-Date: 2026-
|
|
8
|
+
"POT-Creation-Date: 2026-06-16 23:00-0300\n"
|
|
9
9
|
"PO-Revision-Date: 2026-02-24 14:31-0300\n"
|
|
10
10
|
"Last-Translator: Fernando Macedo <fgmacedo@gmail.com>\n"
|
|
11
11
|
"Language: en\n"
|
|
@@ -16,12 +16,12 @@ msgstr ""
|
|
|
16
16
|
"Content-Transfer-Encoding: 8bit\n"
|
|
17
17
|
"Generated-By: Babel 2.18.0\n"
|
|
18
18
|
|
|
19
|
-
#: statemachine/callbacks.py:
|
|
19
|
+
#: statemachine/callbacks.py:398 statemachine/callbacks.py:403
|
|
20
20
|
#, python-brace-format
|
|
21
21
|
msgid "Did not found name '{}' from model or statemachine"
|
|
22
22
|
msgstr "Did not found name '{}' from model or statemachine"
|
|
23
23
|
|
|
24
|
-
#: statemachine/configuration.py:
|
|
24
|
+
#: statemachine/configuration.py:128
|
|
25
25
|
msgid ""
|
|
26
26
|
"There's no current state set. In async code, did you activate the initial"
|
|
27
27
|
" state? (e.g., `await sm.activate_initial_state()`)"
|
|
@@ -29,12 +29,12 @@ msgstr ""
|
|
|
29
29
|
"There's no current state set. In async code, did you activate the initial"
|
|
30
30
|
" state? (e.g., `await sm.activate_initial_state()`)"
|
|
31
31
|
|
|
32
|
-
#: statemachine/dispatcher.py:
|
|
32
|
+
#: statemachine/dispatcher.py:123
|
|
33
33
|
#, python-brace-format
|
|
34
34
|
msgid "Failed to parse boolean expression '{}'"
|
|
35
35
|
msgstr "Failed to parse boolean expression '{}'"
|
|
36
36
|
|
|
37
|
-
#: statemachine/event.py:
|
|
37
|
+
#: statemachine/event.py:94
|
|
38
38
|
#, python-brace-format
|
|
39
39
|
msgid ""
|
|
40
40
|
"Event() received a non-string 'id' ({cls_name}). To combine multiple "
|
|
@@ -43,12 +43,12 @@ msgstr ""
|
|
|
43
43
|
"Event() received a non-string 'id' ({cls_name}). To combine multiple "
|
|
44
44
|
"transitions under one event, use the | operator: t1 | t2."
|
|
45
45
|
|
|
46
|
-
#: statemachine/event.py:
|
|
46
|
+
#: statemachine/event.py:130
|
|
47
47
|
#, python-brace-format
|
|
48
48
|
msgid "Cannot add callback '{}' to an event with no transitions."
|
|
49
49
|
msgstr "Cannot add callback '{}' to an event with no transitions."
|
|
50
50
|
|
|
51
|
-
#: statemachine/event.py:
|
|
51
|
+
#: statemachine/event.py:163
|
|
52
52
|
#, python-brace-format
|
|
53
53
|
msgid "Event {} cannot be called without a SM instance"
|
|
54
54
|
msgstr "Event {} cannot be called without a SM instance"
|
|
@@ -63,7 +63,7 @@ msgstr "{!r} is not a valid state value."
|
|
|
63
63
|
msgid "Can't {} when in {}."
|
|
64
64
|
msgstr "Can't {} when in {}."
|
|
65
65
|
|
|
66
|
-
#: statemachine/factory.py:
|
|
66
|
+
#: statemachine/factory.py:77
|
|
67
67
|
#, python-brace-format
|
|
68
68
|
msgid ""
|
|
69
69
|
"There should be one and only one initial state. Your currently have "
|
|
@@ -72,7 +72,7 @@ msgstr ""
|
|
|
72
72
|
"There should be one and only one initial state. Your currently have "
|
|
73
73
|
"these: {0}"
|
|
74
74
|
|
|
75
|
-
#: statemachine/factory.py:
|
|
75
|
+
#: statemachine/factory.py:194
|
|
76
76
|
#, python-brace-format
|
|
77
77
|
msgid ""
|
|
78
78
|
"There should be one and only one initial state. You currently have these:"
|
|
@@ -81,12 +81,12 @@ msgstr ""
|
|
|
81
81
|
"There should be one and only one initial state. You currently have these:"
|
|
82
82
|
" {!r}"
|
|
83
83
|
|
|
84
|
-
#: statemachine/factory.py:
|
|
84
|
+
#: statemachine/factory.py:209
|
|
85
85
|
#, python-brace-format
|
|
86
86
|
msgid "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
87
87
|
msgstr "Cannot declare transitions from final state. Invalid state(s): {}"
|
|
88
88
|
|
|
89
|
-
#: statemachine/factory.py:
|
|
89
|
+
#: statemachine/factory.py:221
|
|
90
90
|
#, python-brace-format
|
|
91
91
|
msgid ""
|
|
92
92
|
"All non-final states should have at least one outgoing transition. These "
|
|
@@ -95,7 +95,7 @@ msgstr ""
|
|
|
95
95
|
"All non-final states should have at least one outgoing transition. These "
|
|
96
96
|
"states have no outgoing transition: {!r}"
|
|
97
97
|
|
|
98
|
-
#: statemachine/factory.py:
|
|
98
|
+
#: statemachine/factory.py:235
|
|
99
99
|
#, python-brace-format
|
|
100
100
|
msgid ""
|
|
101
101
|
"All non-final states should have at least one path to a final state. "
|
|
@@ -104,7 +104,7 @@ msgstr ""
|
|
|
104
104
|
"All non-final states should have at least one path to a final state. "
|
|
105
105
|
"These states have no path to a final state: {!r}"
|
|
106
106
|
|
|
107
|
-
#: statemachine/factory.py:
|
|
107
|
+
#: statemachine/factory.py:248
|
|
108
108
|
#, python-brace-format
|
|
109
109
|
msgid ""
|
|
110
110
|
"There are unreachable states. The statemachine graph should have a single"
|
|
@@ -113,7 +113,7 @@ msgstr ""
|
|
|
113
113
|
"There are unreachable states. The statemachine graph should have a single"
|
|
114
114
|
" component. Disconnected states: {}"
|
|
115
115
|
|
|
116
|
-
#: statemachine/factory.py:
|
|
116
|
+
#: statemachine/factory.py:285
|
|
117
117
|
#, python-brace-format
|
|
118
118
|
msgid ""
|
|
119
119
|
"Invalid entry in 'listeners': {!r}. Expected a class, callable, or "
|
|
@@ -122,7 +122,7 @@ msgstr ""
|
|
|
122
122
|
"Invalid entry in 'listeners': {!r}. Expected a class, callable, or "
|
|
123
123
|
"listener instance."
|
|
124
124
|
|
|
125
|
-
#: statemachine/factory.py:
|
|
125
|
+
#: statemachine/factory.py:384
|
|
126
126
|
#, python-brace-format
|
|
127
127
|
msgid "An event in the '{}' has no id."
|
|
128
128
|
msgstr "An event in the '{}' has no id."
|
|
@@ -132,20 +132,20 @@ msgstr "An event in the '{}' has no id."
|
|
|
132
132
|
msgid "{!r} is not a valid state machine name."
|
|
133
133
|
msgstr "{!r} is not a valid state machine name."
|
|
134
134
|
|
|
135
|
-
#: statemachine/state.py:
|
|
135
|
+
#: statemachine/state.py:249
|
|
136
136
|
msgid "'donedata' can only be specified on final states."
|
|
137
137
|
msgstr "'donedata' can only be specified on final states."
|
|
138
138
|
|
|
139
|
-
#: statemachine/statemachine.py:
|
|
139
|
+
#: statemachine/statemachine.py:167
|
|
140
140
|
msgid "There are no states or transitions."
|
|
141
141
|
msgstr "There are no states or transitions."
|
|
142
142
|
|
|
143
|
-
#: statemachine/statemachine.py:
|
|
143
|
+
#: statemachine/statemachine.py:237
|
|
144
144
|
#, python-brace-format
|
|
145
145
|
msgid "State overriding is not allowed. Trying to add '{}' to {}"
|
|
146
146
|
msgstr "State overriding is not allowed. Trying to add '{}' to {}"
|
|
147
147
|
|
|
148
|
-
#: statemachine/transition.py:
|
|
148
|
+
#: statemachine/transition.py:74
|
|
149
149
|
#, python-brace-format
|
|
150
150
|
msgid ""
|
|
151
151
|
"Not a valid internal transition from source {source!r}, target {target!r}"
|