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,27 +1,35 @@
1
- """SCXML-specific invoke handler.
1
+ """Format-neutral invoke handler.
2
2
 
3
- Implements the IInvoke protocol by resolving child SCXML content (inline or
4
- via src/srcexpr), evaluating params/namelist in the parent context, and managing
5
- the child machine lifecycle including ``#_parent`` routing, autoforward, and
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 ...invoke import IInvoke
17
- from ...invoke import InvokeContext
20
+ from ..invoke import IInvoke
21
+ from ..invoke import InvokeContext
18
22
  from .actions import ExecuteBlock
19
- from .actions import _eval
20
- from .schema import InvokeDefinition
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
- _VALID_INVOKE_TYPES = {
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 SCXMLInvoker:
34
- """SCXML-specific invoke handler implementing the IInvoke protocol.
41
+ class Invoker:
42
+ """Invoke handler implementing the IInvoke protocol.
35
43
 
36
- Resolves the child SCXML from inline content, src file, or srcexpr,
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 _VALID_INVOKE_TYPES:
94
+ if invoke_type not in VALID_INVOKE_TYPES:
75
95
  raise ValueError(
76
- f"Unsupported invoke type: {invoke_type}. Supported types: {_VALID_INVOKE_TYPES}"
96
+ f"Unsupported invoke type: {invoke_type}. Supported types: {VALID_INVOKE_TYPES}"
77
97
  )
78
98
 
79
- # Resolve child SCXML content
80
- scxml_content = self._resolve_content(machine)
81
- if scxml_content is None:
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(scxml_content, ctx.invokeid)
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 processor) can pop them and store them on the machine instance.
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 ...invoke import _stop_child_machine
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) -> "str | None":
143
- """Resolve the child SCXML content from content/src/srcexpr."""
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
- # Content could be an expr to evaluate or inline SCXML
148
- if defn.content.lstrip().startswith("<"):
149
- return defn.content
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(defn.content, machine=machine)
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 SCXML file
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, scxml_content: str, invokeid: str):
196
- """Parse the child SCXML and create a machine class."""
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(scxml_content, child_name)
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, ``SCXMLInvoker._child`` is set
206
- during the first state entry — *before* the processing loop starts spinning —
207
- so that ``on_event()`` can forward events to the child immediately.
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: "SCXMLInvoker"):
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(SCXMLInvoker.__new__(SCXMLInvoker), IInvoke)
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)