python-statemachine 3.1.2__py3-none-any.whl → 3.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.1.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +5 -11
  6. statemachine/configuration.py +5 -6
  7. statemachine/contrib/diagram/extract.py +26 -27
  8. statemachine/contrib/diagram/formatter.py +5 -7
  9. statemachine/contrib/diagram/model.py +9 -11
  10. statemachine/contrib/diagram/renderers/dot.py +20 -26
  11. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  12. statemachine/contrib/diagram/renderers/table.py +7 -9
  13. statemachine/contrib/weighted.py +7 -11
  14. statemachine/dispatcher.py +18 -13
  15. statemachine/engines/async_.py +20 -9
  16. statemachine/engines/base.py +26 -17
  17. statemachine/event.py +1 -2
  18. statemachine/exceptions.py +2 -2
  19. statemachine/factory.py +11 -15
  20. statemachine/graph.py +2 -2
  21. statemachine/invoke.py +12 -11
  22. statemachine/io/__init__.py +52 -225
  23. statemachine/io/actions.py +627 -0
  24. statemachine/io/builder.py +195 -0
  25. statemachine/io/class_factory.py +236 -0
  26. statemachine/io/evaluators.py +303 -0
  27. statemachine/io/interpreter.py +128 -0
  28. statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
  29. statemachine/io/json/__init__.py +1 -0
  30. statemachine/io/json/reader.py +27 -0
  31. statemachine/io/loader.py +175 -0
  32. statemachine/io/model.py +268 -0
  33. statemachine/io/native.py +402 -0
  34. statemachine/io/ports.py +83 -0
  35. statemachine/io/schemas/statechart.schema.json +258 -0
  36. statemachine/io/scxml/__init__.py +15 -0
  37. statemachine/io/scxml/processor.py +23 -253
  38. statemachine/io/scxml/{parser.py → reader.py} +88 -56
  39. statemachine/io/system_variables.py +199 -0
  40. statemachine/io/validation.py +44 -0
  41. statemachine/io/yaml/__init__.py +1 -0
  42. statemachine/io/yaml/reader.py +65 -0
  43. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  44. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/orderedset.py +15 -6
  48. statemachine/registry.py +1 -4
  49. statemachine/signature.py +2 -5
  50. statemachine/spec_parser.py +228 -45
  51. statemachine/state.py +7 -8
  52. statemachine/statemachine.py +18 -20
  53. statemachine/states.py +3 -5
  54. statemachine/transition.py +3 -4
  55. statemachine/transition_list.py +4 -5
  56. statemachine/transition_mixin.py +1 -1
  57. python_statemachine-3.1.2.dist-info/RECORD +0 -58
  58. statemachine/io/scxml/actions.py +0 -650
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,31 +1,32 @@
1
1
  import re
2
2
  import xml.etree.ElementTree as ET
3
- from typing import List
3
+ from collections.abc import Callable
4
4
  from typing import Literal
5
- from typing import Set
6
5
  from typing import cast
7
- from urllib.parse import urlparse
8
-
9
- from .schema import Action
10
- from .schema import AssignAction
11
- from .schema import CancelAction
12
- from .schema import DataItem
13
- from .schema import DataModel
14
- from .schema import DoneData
15
- from .schema import ExecutableContent
16
- from .schema import ForeachAction
17
- from .schema import HistoryState
18
- from .schema import IfAction
19
- from .schema import IfBranch
20
- from .schema import InvokeDefinition
21
- from .schema import LogAction
22
- from .schema import Param
23
- from .schema import RaiseAction
24
- from .schema import ScriptAction
25
- from .schema import SendAction
26
- from .schema import State
27
- from .schema import StateMachineDefinition
28
- from .schema import Transition
6
+
7
+ from ...exceptions import InvalidDefinition
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,14 +41,33 @@ 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") -> List[str]:
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()
47
48
 
48
49
 
50
+ class _NoDTDTreeBuilder(ET.TreeBuilder):
51
+ """A tree builder that refuses documents carrying a ``<!DOCTYPE>``/DTD.
52
+
53
+ Internal entity definitions (the basis of billion-laughs / quadratic-blowup
54
+ expansion bombs) require a DTD, so rejecting the DOCTYPE neutralizes them at
55
+ parse time. Expat calls this ``doctype`` hook when it meets the declaration.
56
+ """
57
+
58
+ def doctype(self, name, pubid, system): # noqa: D102 - expat callback
59
+ raise InvalidDefinition(
60
+ "DOCTYPE/DTD is not allowed in SCXML documents (XML entity-expansion protection)."
61
+ )
62
+
63
+
64
+ def _fromstring_no_dtd(scxml_content: str) -> ET.Element:
65
+ """Parse SCXML text rejecting any DOCTYPE (see :class:`_NoDTDTreeBuilder`)."""
66
+ return ET.fromstring(scxml_content, parser=ET.XMLParser(target=_NoDTDTreeBuilder()))
67
+
68
+
49
69
  def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
50
- root = ET.fromstring(scxml_content)
70
+ root = _fromstring_no_dtd(scxml_content)
51
71
  strip_namespaces(root)
52
72
 
53
73
  scxml = root if root.tag == "scxml" else root.find(".//scxml")
@@ -87,13 +107,13 @@ def parse_scxml(scxml_content: str) -> StateMachineDefinition: # noqa: C901
87
107
  return definition
88
108
 
89
109
 
90
- def _find_own_datamodel_elements(root: ET.Element) -> List[ET.Element]:
110
+ def _find_own_datamodel_elements(root: ET.Element) -> list[ET.Element]:
91
111
  """Find <datamodel> elements that belong to this SCXML document, not to inline children.
92
112
 
93
113
  Skips any <datamodel> nested inside <content> elements (which contain inline
94
114
  child SCXML documents for <invoke>).
95
115
  """
96
- result: List[ET.Element] = []
116
+ result: list[ET.Element] = []
97
117
 
98
118
  def _walk(elem: ET.Element):
99
119
  for child in elem:
@@ -113,16 +133,12 @@ def parse_datamodel(root: ET.Element) -> "DataModel | None":
113
133
  for datamodel_elem in _find_own_datamodel_elements(root):
114
134
  for data_elem in datamodel_elem.findall("data"):
115
135
  content = data_elem.text and re.sub(r"\s+", " ", data_elem.text).strip() or None
116
- src = data_elem.attrib.get("src")
117
- src_parsed = urlparse(src) if src else None
118
- if src_parsed and src_parsed.scheme == "file" and content is None:
119
- with open(src_parsed.path) as f:
120
- content = f.read()
121
-
136
+ # An external ``src`` is kept as a raw string; resolving it (reading the file)
137
+ # is deferred to datamodel compilation, where the trust policy can gate it.
122
138
  data_model.data.append(
123
139
  DataItem(
124
140
  id=data_elem.attrib["id"],
125
- src=src_parsed,
141
+ src=data_elem.attrib.get("src"),
126
142
  expr=data_elem.attrib.get("expr"),
127
143
  content=content,
128
144
  )
@@ -157,7 +173,7 @@ def parse_history(state_elem: ET.Element) -> HistoryState:
157
173
 
158
174
  def parse_state( # noqa: C901
159
175
  state_elem: ET.Element,
160
- initial_states: Set[str],
176
+ initial_states: set[str],
161
177
  is_final: bool = False,
162
178
  is_parallel: bool = False,
163
179
  ) -> State:
@@ -279,25 +295,11 @@ def parse_executable_content(element: ET.Element) -> ExecutableContent:
279
295
 
280
296
 
281
297
  def parse_element(element: ET.Element) -> Action:
282
- tag = element.tag
283
- if tag == "raise":
284
- return parse_raise(element)
285
- elif tag == "assign":
286
- return parse_assign(element)
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}")
298
+ try:
299
+ parser = _ELEMENT_PARSERS[element.tag]
300
+ except KeyError:
301
+ raise ValueError(f"Unknown tag: {element.tag}") from None
302
+ return parser(element)
301
303
 
302
304
 
303
305
  def parse_raise(element: ET.Element) -> RaiseAction:
@@ -434,7 +436,7 @@ def parse_invoke(element: ET.Element) -> InvokeDefinition:
434
436
  autoforward = element.attrib.get("autoforward", "false").lower() == "true"
435
437
  namelist = element.attrib.get("namelist")
436
438
 
437
- params: List[Param] = []
439
+ params: list[Param] = []
438
440
  content: "str | None" = None
439
441
  finalize: "ExecutableContent | None" = None
440
442
 
@@ -471,3 +473,33 @@ def parse_invoke(element: ET.Element) -> InvokeDefinition:
471
473
  content=content,
472
474
  finalize=finalize,
473
475
  )
476
+
477
+
478
+ #: Dispatch table for executable-content elements (all parsers share the same
479
+ #: ``(element) -> Action`` signature, so a lookup is cleaner than a branch chain).
480
+ _ELEMENT_PARSERS: "dict[str, Callable[[ET.Element], Action]]" = {
481
+ "raise": parse_raise,
482
+ "assign": parse_assign,
483
+ "log": parse_log,
484
+ "if": parse_if,
485
+ "send": parse_send,
486
+ "script": parse_script,
487
+ "foreach": parse_foreach,
488
+ "cancel": parse_cancel,
489
+ }
490
+
491
+
492
+ class SCXMLReader:
493
+ """Format adapter that parses SCXML (XML) documents into the neutral IR."""
494
+
495
+ def read(self, text: str, *, source_name: "str | None" = None) -> StateMachineDefinition:
496
+ return parse_scxml(text)
497
+
498
+
499
+ register_format(
500
+ FormatSpec(
501
+ name="scxml",
502
+ extensions=(".scxml", ".xml"),
503
+ reader_factory=SCXMLReader,
504
+ )
505
+ )
@@ -0,0 +1,199 @@
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
+ """The ``_event.data`` payload view.
22
+
23
+ The raw payload dict is stored under a private (``_``-prefixed) name so a restricted
24
+ expression cannot hand back the underlying dict via ``_event.data.kwargs``; only the
25
+ individual payload fields (``_event.data.<field>``) are reachable.
26
+ """
27
+
28
+ _kwargs: dict
29
+
30
+ def __getattr__(self, name):
31
+ if name == "_kwargs":
32
+ # Guard against unbounded recursion before the field is set (e.g. copy/pickle).
33
+ raise AttributeError(name)
34
+ return self._kwargs.get(name, None)
35
+
36
+ def get(self, name, default=None):
37
+ return self._kwargs.get(name, default)
38
+
39
+
40
+ class OriginTypeSCXML(str):
41
+ """The origintype of an :ref:`Event` as specified by the SCXML namespace."""
42
+
43
+ def __eq__(self, other):
44
+ return other == "http://www.w3.org/TR/scxml/#SCXMLEventProcessor" or other == "scxml"
45
+
46
+
47
+ class EventDataWrapper:
48
+ """The ``_event`` system variable: a read-only view of the current event.
49
+
50
+ Exposes ``name``/``data``/``type``/``origintype``/``sendid``/``invokeid`` following
51
+ the SCXML event model, which is the library's execution model regardless of the
52
+ source syntax.
53
+ """
54
+
55
+ origin: str = ""
56
+ origintype: str = OriginTypeSCXML("scxml")
57
+ invokeid: str = ""
58
+ """If this event is generated from an invoked child process, the Processor MUST set
59
+ this field to the invoke id of the invocation that triggered the child process.
60
+ Otherwise it MUST leave it blank.
61
+ """
62
+
63
+ def __init__(self, event_data=None, *, trigger_data=None):
64
+ # The engine carriers are stored under private (``_``-prefixed) names so a
65
+ # restricted expression cannot reach them (``_event.event_data`` /
66
+ # ``_event.trigger_data``) and pivot to the machine/interpreter/State. The
67
+ # restricted evaluator blocks attribute access to ``_``-prefixed names, and this
68
+ # class deliberately exposes NO public attribute or ``__getattr__`` forward that
69
+ # would hand them back (GHSA-v3qq-3xvg-m77g).
70
+ self._event_data = event_data
71
+ if trigger_data is not None:
72
+ self._trigger_data = trigger_data
73
+ elif event_data is not None:
74
+ self._trigger_data = event_data.trigger_data
75
+ else:
76
+ raise ValueError("Either event_data or trigger_data must be provided")
77
+
78
+ td = self._trigger_data
79
+ self.sendid = td.send_id
80
+ self.invokeid = td.kwargs.get("_invokeid", "")
81
+ if td.event is None or td.event.internal:
82
+ if "error.execution" == td.event:
83
+ self.type = "platform"
84
+ else:
85
+ self.type = "internal"
86
+ self.origintype = ""
87
+ else:
88
+ self.type = "external"
89
+
90
+ @classmethod
91
+ def from_trigger_data(cls, trigger_data):
92
+ """Create an EventDataWrapper directly from a TriggerData (no EventData needed)."""
93
+ return cls(trigger_data=trigger_data)
94
+
95
+ def __eq__(self, value):
96
+ "This makes SCXML test 329 pass. It assumes that the event is the same instance"
97
+ return isinstance(value, EventDataWrapper)
98
+
99
+ @property
100
+ def name(self):
101
+ if self._event_data is not None:
102
+ return self._event_data.event
103
+ return str(self._trigger_data.event) if self._trigger_data.event else None
104
+
105
+ @property
106
+ def data(self):
107
+ "Property used to access the event payload (the SCXML ``_event.data``)."
108
+ td = self._trigger_data
109
+ if td.kwargs:
110
+ return _Data(td.kwargs)
111
+ elif td.args and len(td.args) == 1:
112
+ return td.args[0]
113
+ elif td.args:
114
+ return td.args
115
+ else:
116
+ return None
117
+
118
+
119
+ class IOProcessor:
120
+ """The ``_ioprocessors`` system variable for a session.
121
+
122
+ A minimal Event I/O Processor handle: indexing by any processor name returns itself,
123
+ and ``location`` is the machine name.
124
+ """
125
+
126
+ def __init__(self, interpreter, machine):
127
+ # Stored under private (``_``-prefixed) names so a restricted expression cannot
128
+ # reach the interpreter/machine via ``_ioprocessors.interpreter`` /
129
+ # ``_ioprocessors.machine`` and mutate the process-shared engine
130
+ # (GHSA-v3qq-3xvg-m77g). Only the SCXML-visible ``location`` stays public.
131
+ self._interpreter = interpreter
132
+ self._machine = machine
133
+
134
+ def __getitem__(self, name: str):
135
+ return self
136
+
137
+ @property
138
+ def location(self):
139
+ return self._machine.name
140
+
141
+ def get(self, name: str):
142
+ return getattr(self, name)
143
+
144
+
145
+ @dataclass
146
+ class SessionData:
147
+ """Per-machine runtime session state held by the interpreter."""
148
+
149
+ machine: Any
150
+ processor: IOProcessor
151
+ first_event_raised: bool = False
152
+
153
+ def __post_init__(self):
154
+ self.session_id = f"{self.machine.name}:{id(self.machine)}"
155
+
156
+
157
+ def build_system_variables(machine, session_data: SessionData, event, event_data) -> dict:
158
+ """Compute the system variables to inject into callbacks for the current event.
159
+
160
+ ``_event`` is exposed only after the first real (non-``__initial__``) event, matching
161
+ the SCXML rule that ``_event`` is unbound during the initial macrostep.
162
+ """
163
+ if not session_data.first_event_raised and event and event != "__initial__":
164
+ session_data.first_event_raised = True
165
+
166
+ _event: "EventDataWrapper | None" = None
167
+ if session_data.first_event_raised:
168
+ _event = EventDataWrapper(event_data)
169
+
170
+ return {
171
+ "_name": machine.name,
172
+ "_sessionid": session_data.session_id,
173
+ "_ioprocessors": session_data.processor,
174
+ "_event": _event,
175
+ }
176
+
177
+
178
+ def create_invoke_init_callable():
179
+ """Create a callback that extracts invoke-specific kwargs and stores them on the machine.
180
+
181
+ Inserted at position 0 in the initial state's onentry list for invoked children, so
182
+ ``_invoke_session`` and ``_invoke_params`` are handled before any other callbacks run,
183
+ even for machines without a datamodel.
184
+ """
185
+ initialized = False
186
+
187
+ def invoke_init(*args, **kwargs):
188
+ nonlocal initialized
189
+ if initialized:
190
+ return
191
+ initialized = True
192
+ machine = kwargs.get("machine")
193
+ if machine is not None:
194
+ # Use get() not pop(): each callback receives a copy of kwargs
195
+ # (via EventData.extended_kwargs), so pop would be misleading.
196
+ machine._invoke_params = kwargs.get("_invoke_params")
197
+ machine._invoke_session = kwargs.get("_invoke_session")
198
+
199
+ 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-05-18 16:16-0300\n"
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:404 statemachine/callbacks.py:409
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:129
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:126
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:95
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:131
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:164
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:81
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:198
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:213
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:225
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:239
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:252
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:289
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:388
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:250
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:169
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:239
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:75
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}"