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
statemachine/io/scxml/actions.py
DELETED
|
@@ -1,650 +0,0 @@
|
|
|
1
|
-
import html
|
|
2
|
-
import logging
|
|
3
|
-
import re
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from itertools import chain
|
|
6
|
-
from typing import Any
|
|
7
|
-
from typing import Callable
|
|
8
|
-
from uuid import uuid4
|
|
9
|
-
|
|
10
|
-
from ...event import BoundEvent
|
|
11
|
-
from ...event import Event
|
|
12
|
-
from ...event import _event_data_kwargs
|
|
13
|
-
from ...spec_parser import InState
|
|
14
|
-
from ...statemachine import StateChart
|
|
15
|
-
from .parser import Action
|
|
16
|
-
from .parser import AssignAction
|
|
17
|
-
from .parser import IfAction
|
|
18
|
-
from .parser import LogAction
|
|
19
|
-
from .parser import RaiseAction
|
|
20
|
-
from .parser import SendAction
|
|
21
|
-
from .schema import CancelAction
|
|
22
|
-
from .schema import DataItem
|
|
23
|
-
from .schema import DataModel
|
|
24
|
-
from .schema import DoneData
|
|
25
|
-
from .schema import ExecutableContent
|
|
26
|
-
from .schema import ForeachAction
|
|
27
|
-
from .schema import Param
|
|
28
|
-
from .schema import ScriptAction
|
|
29
|
-
|
|
30
|
-
logger = logging.getLogger(__name__)
|
|
31
|
-
_debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None
|
|
32
|
-
protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ParseTime:
|
|
36
|
-
pattern = re.compile(r"(\d+)?(\.\d+)?(s|ms)")
|
|
37
|
-
|
|
38
|
-
@classmethod
|
|
39
|
-
def parse_delay(cls, delay: "str | None", delayexpr: "str | None", **kwargs):
|
|
40
|
-
if delay:
|
|
41
|
-
return cls.time_in_ms(delay)
|
|
42
|
-
elif delayexpr:
|
|
43
|
-
delay_expr_expanded = cls.replace(delayexpr)
|
|
44
|
-
return cls.time_in_ms(_eval(delay_expr_expanded, **kwargs))
|
|
45
|
-
|
|
46
|
-
return 0
|
|
47
|
-
|
|
48
|
-
@classmethod
|
|
49
|
-
def replace(cls, expr: str) -> str:
|
|
50
|
-
def rep(match):
|
|
51
|
-
return str(cls.time_in_ms(match.group(0)))
|
|
52
|
-
|
|
53
|
-
return cls.pattern.sub(rep, expr)
|
|
54
|
-
|
|
55
|
-
@classmethod
|
|
56
|
-
def time_in_ms(cls, expr: str) -> float:
|
|
57
|
-
"""
|
|
58
|
-
Convert a CSS2 time expression to milliseconds.
|
|
59
|
-
|
|
60
|
-
Args:
|
|
61
|
-
time (str): A string representing the time, e.g., '1.5s' or '150ms'.
|
|
62
|
-
|
|
63
|
-
Returns:
|
|
64
|
-
float: The time in milliseconds.
|
|
65
|
-
|
|
66
|
-
Raises:
|
|
67
|
-
ValueError: If the input is not a valid CSS2 time expression.
|
|
68
|
-
"""
|
|
69
|
-
if expr.endswith("ms"):
|
|
70
|
-
try:
|
|
71
|
-
return float(expr[:-2])
|
|
72
|
-
except ValueError as e:
|
|
73
|
-
raise ValueError(f"Invalid time value: {expr}") from e
|
|
74
|
-
elif expr.endswith("s"):
|
|
75
|
-
try:
|
|
76
|
-
return float(expr[:-1]) * 1000
|
|
77
|
-
except ValueError as e:
|
|
78
|
-
raise ValueError(f"Invalid time value: {expr}") from e
|
|
79
|
-
else:
|
|
80
|
-
try:
|
|
81
|
-
return float(expr)
|
|
82
|
-
except ValueError as e:
|
|
83
|
-
raise ValueError(f"Invalid time unit in: {expr}") from e
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
@dataclass
|
|
87
|
-
class _Data:
|
|
88
|
-
kwargs: dict
|
|
89
|
-
|
|
90
|
-
def __getattr__(self, name):
|
|
91
|
-
return self.kwargs.get(name, None)
|
|
92
|
-
|
|
93
|
-
def get(self, name, default=None):
|
|
94
|
-
return self.kwargs.get(name, default)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class OriginTypeSCXML(str):
|
|
98
|
-
"""The origintype of the :ref:`Event` as specified by the SCXML namespace."""
|
|
99
|
-
|
|
100
|
-
def __eq__(self, other):
|
|
101
|
-
return other == "http://www.w3.org/TR/scxml/#SCXMLEventProcessor" or other == "scxml"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class EventDataWrapper:
|
|
105
|
-
origin: str = ""
|
|
106
|
-
origintype: str = OriginTypeSCXML("scxml")
|
|
107
|
-
"""The origintype of the :ref:`Event` as specified by the SCXML namespace."""
|
|
108
|
-
invokeid: str = ""
|
|
109
|
-
"""If this event is generated from an invoked child process, the SCXML Processor MUST set
|
|
110
|
-
this field to the invoke id of the invocation that triggered the child process.
|
|
111
|
-
Otherwise it MUST leave it blank.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
def __init__(self, event_data=None, *, trigger_data=None):
|
|
115
|
-
self.event_data = event_data
|
|
116
|
-
if trigger_data is not None:
|
|
117
|
-
self.trigger_data = trigger_data
|
|
118
|
-
elif event_data is not None:
|
|
119
|
-
self.trigger_data = event_data.trigger_data
|
|
120
|
-
else:
|
|
121
|
-
raise ValueError("Either event_data or trigger_data must be provided")
|
|
122
|
-
|
|
123
|
-
td = self.trigger_data
|
|
124
|
-
self.sendid = td.send_id
|
|
125
|
-
self.invokeid = td.kwargs.get("_invokeid", "")
|
|
126
|
-
if td.event is None or td.event.internal:
|
|
127
|
-
if "error.execution" == td.event:
|
|
128
|
-
self.type = "platform"
|
|
129
|
-
else:
|
|
130
|
-
self.type = "internal"
|
|
131
|
-
self.origintype = ""
|
|
132
|
-
else:
|
|
133
|
-
self.type = "external"
|
|
134
|
-
|
|
135
|
-
@classmethod
|
|
136
|
-
def from_trigger_data(cls, trigger_data):
|
|
137
|
-
"""Create an EventDataWrapper directly from a TriggerData (no EventData needed)."""
|
|
138
|
-
return cls(trigger_data=trigger_data)
|
|
139
|
-
|
|
140
|
-
def __getattr__(self, name):
|
|
141
|
-
if self.event_data is not None:
|
|
142
|
-
return getattr(self.event_data, name)
|
|
143
|
-
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
144
|
-
|
|
145
|
-
def __eq__(self, value):
|
|
146
|
-
"This makes SCXML test 329 pass. It assumes that the event is the same instance"
|
|
147
|
-
return isinstance(value, EventDataWrapper)
|
|
148
|
-
|
|
149
|
-
@property
|
|
150
|
-
def name(self):
|
|
151
|
-
if self.event_data is not None:
|
|
152
|
-
return self.event_data.event
|
|
153
|
-
return str(self.trigger_data.event) if self.trigger_data.event else None
|
|
154
|
-
|
|
155
|
-
@property
|
|
156
|
-
def data(self):
|
|
157
|
-
"Property used by the SCXML namespace"
|
|
158
|
-
td = self.trigger_data
|
|
159
|
-
if td.kwargs:
|
|
160
|
-
return _Data(td.kwargs)
|
|
161
|
-
elif td.args and len(td.args) == 1:
|
|
162
|
-
return td.args[0]
|
|
163
|
-
elif td.args:
|
|
164
|
-
return td.args
|
|
165
|
-
else:
|
|
166
|
-
return None
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
def _eval(expr: str, **kwargs) -> Any:
|
|
170
|
-
if "machine" in kwargs:
|
|
171
|
-
kwargs.update(
|
|
172
|
-
**{
|
|
173
|
-
k: v
|
|
174
|
-
for k, v in kwargs["machine"].model.__dict__.items()
|
|
175
|
-
if k not in protected_attrs
|
|
176
|
-
}
|
|
177
|
-
)
|
|
178
|
-
kwargs["In"] = InState(kwargs["machine"])
|
|
179
|
-
return eval(expr, {}, kwargs)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class CallableAction:
|
|
183
|
-
action: Any
|
|
184
|
-
|
|
185
|
-
def __init__(self):
|
|
186
|
-
self.__qualname__ = f"{self.__class__.__module__}.{self.__class__.__name__}"
|
|
187
|
-
|
|
188
|
-
def __call__(self, *args, **kwargs):
|
|
189
|
-
raise NotImplementedError
|
|
190
|
-
|
|
191
|
-
def __str__(self):
|
|
192
|
-
return f"{self.action}"
|
|
193
|
-
|
|
194
|
-
def __repr__(self):
|
|
195
|
-
return f"{self.__class__.__name__}({self.action!r})"
|
|
196
|
-
|
|
197
|
-
@property
|
|
198
|
-
def __name__(self):
|
|
199
|
-
return str(self)
|
|
200
|
-
|
|
201
|
-
@property
|
|
202
|
-
def __code__(self):
|
|
203
|
-
return self.__call__.__code__
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
class Cond(CallableAction):
|
|
207
|
-
"""Evaluates a condition like a predicate and returns True or False."""
|
|
208
|
-
|
|
209
|
-
@classmethod
|
|
210
|
-
def create(cls, cond: "str | None", processor=None):
|
|
211
|
-
cond = cls._normalize(cond)
|
|
212
|
-
if cond is None:
|
|
213
|
-
return None
|
|
214
|
-
|
|
215
|
-
return cls(cond, processor)
|
|
216
|
-
|
|
217
|
-
def __init__(self, cond: str, processor=None):
|
|
218
|
-
super().__init__()
|
|
219
|
-
self.action = cond
|
|
220
|
-
self.processor = processor
|
|
221
|
-
|
|
222
|
-
def __call__(self, *args, **kwargs):
|
|
223
|
-
result = _eval(self.action, **kwargs)
|
|
224
|
-
_debug("Cond %s -> %s", self.action, result)
|
|
225
|
-
return result
|
|
226
|
-
|
|
227
|
-
@staticmethod
|
|
228
|
-
def _normalize(cond: "str | None") -> "str | None":
|
|
229
|
-
"""
|
|
230
|
-
Normalizes a JavaScript-like condition string to be compatible with Python's eval.
|
|
231
|
-
"""
|
|
232
|
-
if cond is None:
|
|
233
|
-
return None
|
|
234
|
-
|
|
235
|
-
# Decode HTML entities, to allow XML syntax like `Var1<Var2`
|
|
236
|
-
cond = html.unescape(cond)
|
|
237
|
-
|
|
238
|
-
replacements = {
|
|
239
|
-
"true": "True",
|
|
240
|
-
"false": "False",
|
|
241
|
-
"null": "None",
|
|
242
|
-
"===": "==",
|
|
243
|
-
"!==": "!=",
|
|
244
|
-
"&&": "and",
|
|
245
|
-
"||": "or",
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
# Use regex to replace each JavaScript-like token with its Python equivalent
|
|
249
|
-
pattern = re.compile(r"\b(?:true|false|null)\b|===|!==|&&|\|\|")
|
|
250
|
-
return pattern.sub(lambda match: replacements[match.group(0)], cond)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def create_action_callable(action: Action) -> Callable:
|
|
254
|
-
if isinstance(action, RaiseAction):
|
|
255
|
-
return create_raise_action_callable(action)
|
|
256
|
-
elif isinstance(action, AssignAction):
|
|
257
|
-
return Assign(action)
|
|
258
|
-
elif isinstance(action, LogAction):
|
|
259
|
-
return Log(action)
|
|
260
|
-
elif isinstance(action, IfAction):
|
|
261
|
-
return create_if_action_callable(action)
|
|
262
|
-
elif isinstance(action, ForeachAction):
|
|
263
|
-
return create_foreach_action_callable(action)
|
|
264
|
-
elif isinstance(action, SendAction):
|
|
265
|
-
return create_send_action_callable(action)
|
|
266
|
-
elif isinstance(action, CancelAction):
|
|
267
|
-
return create_cancel_action_callable(action)
|
|
268
|
-
elif isinstance(action, ScriptAction):
|
|
269
|
-
return create_script_action_callable(action)
|
|
270
|
-
else:
|
|
271
|
-
raise ValueError(f"Unknown action type: {type(action)}")
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class Assign(CallableAction):
|
|
275
|
-
def __init__(self, action: AssignAction):
|
|
276
|
-
super().__init__()
|
|
277
|
-
self.action = action
|
|
278
|
-
|
|
279
|
-
def __call__(self, *args, **kwargs):
|
|
280
|
-
machine: StateChart = kwargs["machine"]
|
|
281
|
-
if self.action.child_xml is not None:
|
|
282
|
-
value = self.action.child_xml
|
|
283
|
-
else:
|
|
284
|
-
value = _eval(self.action.expr, **kwargs)
|
|
285
|
-
|
|
286
|
-
*path, attr = self.action.location.split(".")
|
|
287
|
-
obj = machine.model
|
|
288
|
-
for p in path:
|
|
289
|
-
obj = getattr(obj, p)
|
|
290
|
-
|
|
291
|
-
if not attr.isidentifier() or not (hasattr(obj, attr) or attr in kwargs):
|
|
292
|
-
raise ValueError(
|
|
293
|
-
f"<assign> 'location' must be a valid Python attribute name and must be declared, "
|
|
294
|
-
f"got: {self.action.location}"
|
|
295
|
-
)
|
|
296
|
-
if attr in protected_attrs:
|
|
297
|
-
raise ValueError(
|
|
298
|
-
f"<assign> 'location' cannot assign to a protected attribute: "
|
|
299
|
-
f"{self.action.location}"
|
|
300
|
-
)
|
|
301
|
-
setattr(obj, attr, value)
|
|
302
|
-
_debug("Assign: %s = %r", self.action.location, value)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
class Log(CallableAction):
|
|
306
|
-
def __init__(self, action: LogAction):
|
|
307
|
-
super().__init__()
|
|
308
|
-
self.action = action
|
|
309
|
-
|
|
310
|
-
def __call__(self, *args, **kwargs):
|
|
311
|
-
value = _eval(self.action.expr, **kwargs) if self.action.expr else None
|
|
312
|
-
|
|
313
|
-
if self.action.label and self.action.expr is not None:
|
|
314
|
-
msg = f"{self.action.label}: {value!r}"
|
|
315
|
-
elif self.action.label:
|
|
316
|
-
msg = f"{self.action.label}"
|
|
317
|
-
else:
|
|
318
|
-
msg = f"{value!r}"
|
|
319
|
-
print(msg)
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def create_if_action_callable(action: IfAction) -> Callable:
|
|
323
|
-
branches = [
|
|
324
|
-
(
|
|
325
|
-
Cond.create(branch.cond),
|
|
326
|
-
[create_action_callable(action) for action in branch.actions],
|
|
327
|
-
)
|
|
328
|
-
for branch in action.branches
|
|
329
|
-
]
|
|
330
|
-
|
|
331
|
-
def if_action(*args, **kwargs):
|
|
332
|
-
machine: StateChart = kwargs["machine"]
|
|
333
|
-
for cond, actions in branches:
|
|
334
|
-
try:
|
|
335
|
-
cond_result = not cond or cond(*args, **kwargs)
|
|
336
|
-
except Exception as e:
|
|
337
|
-
# SCXML spec: condition error → treat as false, queue error.execution.
|
|
338
|
-
if machine.catch_errors_as_events:
|
|
339
|
-
machine.send("error.execution", error=e, internal=True)
|
|
340
|
-
cond_result = False
|
|
341
|
-
else:
|
|
342
|
-
raise
|
|
343
|
-
if cond_result:
|
|
344
|
-
for action in actions:
|
|
345
|
-
action(*args, **kwargs)
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
if_action.action = action # type: ignore[attr-defined]
|
|
349
|
-
return if_action
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
def create_foreach_action_callable(action: ForeachAction) -> Callable:
|
|
353
|
-
child_actions = [create_action_callable(act) for act in action.content.actions]
|
|
354
|
-
|
|
355
|
-
def foreach_action(*args, **kwargs):
|
|
356
|
-
machine: StateChart = kwargs["machine"]
|
|
357
|
-
try:
|
|
358
|
-
# Evaluate the array expression to get the iterable
|
|
359
|
-
array = _eval(action.array, **kwargs)
|
|
360
|
-
except Exception as e:
|
|
361
|
-
raise ValueError(f"Error evaluating <foreach> 'array' expression: {e}") from e
|
|
362
|
-
|
|
363
|
-
if not action.item.isidentifier():
|
|
364
|
-
raise ValueError(
|
|
365
|
-
f"<foreach> 'item' must be a valid Python attribute name, got: {action.item}"
|
|
366
|
-
)
|
|
367
|
-
for index, item in enumerate(array):
|
|
368
|
-
# Assign the item and optionally the index
|
|
369
|
-
setattr(machine.model, action.item, item)
|
|
370
|
-
if action.index:
|
|
371
|
-
setattr(machine.model, action.index, index)
|
|
372
|
-
|
|
373
|
-
# Execute child actions
|
|
374
|
-
for act in child_actions:
|
|
375
|
-
act(*args, **kwargs)
|
|
376
|
-
|
|
377
|
-
foreach_action.action = action # type: ignore[attr-defined]
|
|
378
|
-
return foreach_action
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def create_raise_action_callable(action: RaiseAction) -> Callable:
|
|
382
|
-
def raise_action(*args, **kwargs):
|
|
383
|
-
machine: StateChart = kwargs["machine"]
|
|
384
|
-
|
|
385
|
-
Event(id=action.event, internal=True, _sm=machine).put()
|
|
386
|
-
|
|
387
|
-
raise_action.action = action # type: ignore[attr-defined]
|
|
388
|
-
return raise_action
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
def _send_to_parent(action: SendAction, **kwargs):
|
|
392
|
-
"""Route a <send target="#_parent"> to the parent machine via _invoke_session."""
|
|
393
|
-
machine = kwargs["machine"]
|
|
394
|
-
session = getattr(machine, "_invoke_session", None)
|
|
395
|
-
if session is None:
|
|
396
|
-
logger.warning(
|
|
397
|
-
"<send target='#_parent'> ignored: machine %r has no _invoke_session",
|
|
398
|
-
machine.name,
|
|
399
|
-
)
|
|
400
|
-
return
|
|
401
|
-
event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
|
|
402
|
-
names = []
|
|
403
|
-
for name in (action.namelist or "").strip().split():
|
|
404
|
-
if not hasattr(machine.model, name):
|
|
405
|
-
raise NameError(f"Namelist variable '{name}' not found on model")
|
|
406
|
-
names.append(Param(name=name, expr=name))
|
|
407
|
-
params_values = {}
|
|
408
|
-
for param in chain(names, action.params):
|
|
409
|
-
if param.expr is None:
|
|
410
|
-
continue
|
|
411
|
-
params_values[param.name] = _eval(param.expr, **kwargs)
|
|
412
|
-
session.send_to_parent(event, **params_values)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def _send_to_invoke(action: SendAction, invokeid: str, **kwargs):
|
|
416
|
-
"""Route a <send target="#_<invokeid>"> to the invoked child session."""
|
|
417
|
-
machine: StateChart = kwargs["machine"]
|
|
418
|
-
event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
|
|
419
|
-
names = []
|
|
420
|
-
for name in (action.namelist or "").strip().split():
|
|
421
|
-
if not hasattr(machine.model, name):
|
|
422
|
-
raise NameError(f"Namelist variable '{name}' not found on model")
|
|
423
|
-
names.append(Param(name=name, expr=name))
|
|
424
|
-
params_values = {}
|
|
425
|
-
for param in chain(names, action.params):
|
|
426
|
-
if param.expr is None:
|
|
427
|
-
continue
|
|
428
|
-
params_values[param.name] = _eval(param.expr, **kwargs)
|
|
429
|
-
if not machine._engine._invoke_manager.send_to_child(invokeid, event, **params_values):
|
|
430
|
-
# Per SCXML spec: if target is not reachable → error.communication
|
|
431
|
-
BoundEvent("error.communication", internal=True, _sm=machine).put()
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
|
|
435
|
-
content: Any = ()
|
|
436
|
-
_valid_targets = (None, "#_internal", "internal", "#_parent", "parent")
|
|
437
|
-
if action.content:
|
|
438
|
-
try:
|
|
439
|
-
content = (eval(action.content, {}, {}),)
|
|
440
|
-
except (NameError, SyntaxError, TypeError):
|
|
441
|
-
content = (action.content,)
|
|
442
|
-
|
|
443
|
-
def send_action(*args, **kwargs): # noqa: C901
|
|
444
|
-
machine: StateChart = kwargs["machine"]
|
|
445
|
-
event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
|
|
446
|
-
target = action.target if action.target else None
|
|
447
|
-
|
|
448
|
-
if action.type and action.type != "http://www.w3.org/TR/scxml/#SCXMLEventProcessor":
|
|
449
|
-
# Per SCXML spec 6.2.3, unsupported type raises error.execution
|
|
450
|
-
raise ValueError(
|
|
451
|
-
f"Unsupported send type: {action.type}. "
|
|
452
|
-
"Only 'http://www.w3.org/TR/scxml/#SCXMLEventProcessor' is supported"
|
|
453
|
-
)
|
|
454
|
-
if target not in _valid_targets:
|
|
455
|
-
if target and target.startswith("#_scxml_"):
|
|
456
|
-
# Valid SCXML session reference but undispatchable → error.communication
|
|
457
|
-
BoundEvent("error.communication", internal=True, _sm=machine).put()
|
|
458
|
-
elif target and target.startswith("#_"):
|
|
459
|
-
# #_<invokeid> → route to invoked child session
|
|
460
|
-
_send_to_invoke(action, target[2:], **kwargs)
|
|
461
|
-
else:
|
|
462
|
-
# Invalid target expression → error.execution (raised as exception)
|
|
463
|
-
raise ValueError(f"Invalid target: {target}. Must be one of {_valid_targets}")
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
# Handle #_parent target — route to parent via _invoke_session
|
|
467
|
-
if target == "#_parent":
|
|
468
|
-
_send_to_parent(action, **kwargs)
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
internal = target in ("#_internal", "internal")
|
|
472
|
-
|
|
473
|
-
send_id = None
|
|
474
|
-
if action.id:
|
|
475
|
-
send_id = action.id
|
|
476
|
-
elif action.idlocation:
|
|
477
|
-
send_id = uuid4().hex
|
|
478
|
-
setattr(machine.model, action.idlocation, send_id)
|
|
479
|
-
|
|
480
|
-
delay = ParseTime.parse_delay(action.delay, action.delayexpr, **kwargs)
|
|
481
|
-
|
|
482
|
-
# Per SCXML spec, if namelist evaluation causes an error (e.g., variable not found),
|
|
483
|
-
# the send MUST NOT be dispatched and error.execution is raised.
|
|
484
|
-
names = []
|
|
485
|
-
for name in (action.namelist or "").strip().split():
|
|
486
|
-
if not hasattr(machine.model, name):
|
|
487
|
-
raise NameError(f"Namelist variable '{name}' not found on model")
|
|
488
|
-
names.append(Param(name=name, expr=name))
|
|
489
|
-
params_values = {}
|
|
490
|
-
for param in chain(names, action.params):
|
|
491
|
-
if param.expr is None:
|
|
492
|
-
continue
|
|
493
|
-
params_values[param.name] = _eval(param.expr, **kwargs)
|
|
494
|
-
|
|
495
|
-
Event(id=event, delay=delay, internal=internal, _sm=machine).put(
|
|
496
|
-
*content,
|
|
497
|
-
send_id=send_id,
|
|
498
|
-
**params_values,
|
|
499
|
-
)
|
|
500
|
-
|
|
501
|
-
send_action.action = action # type: ignore[attr-defined]
|
|
502
|
-
return send_action
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
def create_cancel_action_callable(action: CancelAction) -> Callable:
|
|
506
|
-
def cancel_action(*args, **kwargs):
|
|
507
|
-
machine: StateChart = kwargs["machine"]
|
|
508
|
-
if action.sendid:
|
|
509
|
-
send_id = action.sendid
|
|
510
|
-
elif action.sendidexpr:
|
|
511
|
-
send_id = _eval(action.sendidexpr, **kwargs)
|
|
512
|
-
else:
|
|
513
|
-
raise ValueError("CancelAction must have either 'sendid' or 'sendidexpr'")
|
|
514
|
-
# Implement cancel logic if necessary
|
|
515
|
-
# For now, we can just print that the event is canceled
|
|
516
|
-
machine.cancel_event(send_id)
|
|
517
|
-
|
|
518
|
-
cancel_action.action = action # type: ignore[attr-defined]
|
|
519
|
-
return cancel_action
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
def create_script_action_callable(action: ScriptAction) -> Callable:
|
|
523
|
-
def script_action(*args, **kwargs):
|
|
524
|
-
machine: StateChart = kwargs["machine"]
|
|
525
|
-
local_vars = {
|
|
526
|
-
**machine.model.__dict__,
|
|
527
|
-
}
|
|
528
|
-
exec(action.content, {}, local_vars)
|
|
529
|
-
|
|
530
|
-
# Assign the resulting variables to the state machine's model
|
|
531
|
-
for var_name, value in local_vars.items():
|
|
532
|
-
setattr(machine.model, var_name, value)
|
|
533
|
-
|
|
534
|
-
script_action.action = action # type: ignore[attr-defined]
|
|
535
|
-
return script_action
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def create_invoke_init_callable() -> Callable:
|
|
539
|
-
"""Create a callback that extracts invoke-specific kwargs and stores them on the machine.
|
|
540
|
-
|
|
541
|
-
This is always inserted at position 0 in the initial state's onentry list by the
|
|
542
|
-
SCXML processor, so that ``_invoke_session`` and ``_invoke_params`` are handled
|
|
543
|
-
before any other callbacks run — even for SMs without a ``<datamodel>``.
|
|
544
|
-
"""
|
|
545
|
-
initialized = False
|
|
546
|
-
|
|
547
|
-
def invoke_init(*args, **kwargs):
|
|
548
|
-
nonlocal initialized
|
|
549
|
-
if initialized:
|
|
550
|
-
return
|
|
551
|
-
initialized = True
|
|
552
|
-
machine = kwargs.get("machine")
|
|
553
|
-
if machine is not None:
|
|
554
|
-
# Use get() not pop(): each callback receives a copy of kwargs
|
|
555
|
-
# (via EventData.extended_kwargs), so pop would be misleading.
|
|
556
|
-
machine._invoke_params = kwargs.get("_invoke_params")
|
|
557
|
-
machine._invoke_session = kwargs.get("_invoke_session")
|
|
558
|
-
|
|
559
|
-
return invoke_init
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
def _create_dataitem_callable(action: DataItem) -> Callable:
|
|
563
|
-
def data_initializer(**kwargs):
|
|
564
|
-
machine: StateChart = kwargs["machine"]
|
|
565
|
-
|
|
566
|
-
# Check for invoke param overrides — params from parent override child defaults
|
|
567
|
-
invoke_params = getattr(machine, "_invoke_params", None)
|
|
568
|
-
if invoke_params and action.id in invoke_params:
|
|
569
|
-
setattr(machine.model, action.id, invoke_params[action.id])
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
if action.expr:
|
|
573
|
-
try:
|
|
574
|
-
value = _eval(action.expr, **kwargs)
|
|
575
|
-
except Exception:
|
|
576
|
-
setattr(machine.model, action.id, None)
|
|
577
|
-
raise
|
|
578
|
-
|
|
579
|
-
elif action.content:
|
|
580
|
-
try:
|
|
581
|
-
value = _eval(action.content, **kwargs)
|
|
582
|
-
except Exception:
|
|
583
|
-
value = action.content
|
|
584
|
-
else:
|
|
585
|
-
value = None
|
|
586
|
-
|
|
587
|
-
setattr(machine.model, action.id, value)
|
|
588
|
-
|
|
589
|
-
return data_initializer
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
def create_datamodel_action_callable(action: DataModel) -> "Callable | None":
|
|
593
|
-
data_elements = [_create_dataitem_callable(item) for item in action.data]
|
|
594
|
-
data_elements.extend([create_script_action_callable(script) for script in action.scripts])
|
|
595
|
-
|
|
596
|
-
if not data_elements:
|
|
597
|
-
return None
|
|
598
|
-
|
|
599
|
-
initialized = False
|
|
600
|
-
|
|
601
|
-
def datamodel(*args, **kwargs):
|
|
602
|
-
nonlocal initialized
|
|
603
|
-
if initialized:
|
|
604
|
-
return
|
|
605
|
-
initialized = True
|
|
606
|
-
|
|
607
|
-
for act in data_elements:
|
|
608
|
-
act(**kwargs)
|
|
609
|
-
|
|
610
|
-
return datamodel
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
class ExecuteBlock(CallableAction):
|
|
614
|
-
"""Parses the children as <executable> content XML into a callable."""
|
|
615
|
-
|
|
616
|
-
def __init__(self, content: ExecutableContent):
|
|
617
|
-
super().__init__()
|
|
618
|
-
self.action = content
|
|
619
|
-
self.action_callables = [create_action_callable(action) for action in content.actions]
|
|
620
|
-
|
|
621
|
-
def __call__(self, *args, **kwargs):
|
|
622
|
-
for action in self.action_callables:
|
|
623
|
-
action(*args, **kwargs)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
class DoneDataCallable(CallableAction):
|
|
627
|
-
"""Evaluates <donedata> params/content and returns the data for done events."""
|
|
628
|
-
|
|
629
|
-
def __init__(self, donedata: DoneData):
|
|
630
|
-
super().__init__()
|
|
631
|
-
self.action = donedata
|
|
632
|
-
self.donedata = donedata
|
|
633
|
-
|
|
634
|
-
def __call__(self, *args, **kwargs):
|
|
635
|
-
if self.donedata.content_expr is not None:
|
|
636
|
-
return _eval(self.donedata.content_expr, **kwargs)
|
|
637
|
-
|
|
638
|
-
result = {}
|
|
639
|
-
for param in self.donedata.params:
|
|
640
|
-
if param.expr is not None:
|
|
641
|
-
result[param.name] = _eval(param.expr, **kwargs)
|
|
642
|
-
elif param.location is not None: # pragma: no branch
|
|
643
|
-
location = param.location.strip()
|
|
644
|
-
try:
|
|
645
|
-
result[param.name] = _eval(location, **kwargs)
|
|
646
|
-
except Exception as e:
|
|
647
|
-
raise ValueError(
|
|
648
|
-
f"<param> location '{location}' does not resolve to a valid value"
|
|
649
|
-
) from e
|
|
650
|
-
return result
|