python-statemachine 3.1.1__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.1.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.1.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/__init__.py +15 -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 +12 -16
- 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 +20 -22
- 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.1.dist-info/RECORD +0 -58
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,49 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
"""Turn neutral-IR :mod:`~statemachine.io.model` actions into executable callables.
|
|
2
|
+
|
|
3
|
+
Each :class:`~statemachine.io.model.Action` is compiled into a callable by
|
|
4
|
+
:func:`create_action_callable`, using an :class:`~statemachine.io.evaluators.Evaluator`
|
|
5
|
+
to evaluate the expression/script strings it carries (secure by default). This layer
|
|
6
|
+
is format-neutral: it is shared by the SCXML, JSON and YAML readers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
2
9
|
import logging
|
|
3
10
|
import re
|
|
4
|
-
from
|
|
11
|
+
from collections.abc import Callable
|
|
5
12
|
from itertools import chain
|
|
6
13
|
from typing import Any
|
|
7
|
-
from typing import Callable
|
|
8
14
|
from uuid import uuid4
|
|
9
15
|
|
|
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 .
|
|
16
|
+
from ..event import BoundEvent
|
|
17
|
+
from ..event import Event
|
|
18
|
+
from ..statemachine import StateChart
|
|
19
|
+
from .evaluators import Evaluator
|
|
20
|
+
from .evaluators import protected_attrs
|
|
21
|
+
from .model import Action
|
|
22
|
+
from .model import AssignAction
|
|
23
|
+
from .model import CancelAction
|
|
24
|
+
from .model import DataItem
|
|
25
|
+
from .model import DataModel
|
|
26
|
+
from .model import DoneData
|
|
27
|
+
from .model import ExecutableContent
|
|
28
|
+
from .model import ForeachAction
|
|
29
|
+
from .model import IfAction
|
|
30
|
+
from .model import LogAction
|
|
31
|
+
from .model import Param
|
|
32
|
+
from .model import RaiseAction
|
|
33
|
+
from .model import ScriptAction
|
|
34
|
+
from .model import SendAction
|
|
29
35
|
|
|
30
36
|
logger = logging.getLogger(__name__)
|
|
31
37
|
_debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None
|
|
32
|
-
protected_attrs = _event_data_kwargs | {"_sessionid", "_ioprocessors", "_name", "_event"}
|
|
33
38
|
|
|
34
39
|
|
|
35
40
|
class ParseTime:
|
|
36
|
-
pattern = re.compile(r"(\d+
|
|
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
|
|
41
|
+
pattern = re.compile(r"(\d+(?:\.\d+)?|\.\d+)(s|ms)")
|
|
42
|
+
"""CSS2 time literal: a number (``5``, ``1.5``, ``.5``) followed by ``s`` or ``ms``."""
|
|
47
43
|
|
|
48
44
|
@classmethod
|
|
49
45
|
def replace(cls, expr: str) -> str:
|
|
@@ -83,102 +79,6 @@ class ParseTime:
|
|
|
83
79
|
raise ValueError(f"Invalid time unit in: {expr}") from e
|
|
84
80
|
|
|
85
81
|
|
|
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
82
|
class CallableAction:
|
|
183
83
|
action: Any
|
|
184
84
|
|
|
@@ -207,81 +107,61 @@ class Cond(CallableAction):
|
|
|
207
107
|
"""Evaluates a condition like a predicate and returns True or False."""
|
|
208
108
|
|
|
209
109
|
@classmethod
|
|
210
|
-
def create(cls, cond: "str | None", processor=None):
|
|
211
|
-
cond = cls._normalize(cond)
|
|
110
|
+
def create(cls, cond: "str | None", evaluator: Evaluator, processor=None):
|
|
212
111
|
if cond is None:
|
|
213
112
|
return None
|
|
113
|
+
return cls(cond, evaluator, processor)
|
|
214
114
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def __init__(self, cond: str, processor=None):
|
|
115
|
+
def __init__(self, cond: str, evaluator: Evaluator, processor=None):
|
|
218
116
|
super().__init__()
|
|
219
117
|
self.action = cond
|
|
220
118
|
self.processor = processor
|
|
119
|
+
self._cond = evaluator.compile_bool(cond)
|
|
221
120
|
|
|
222
121
|
def __call__(self, *args, **kwargs):
|
|
223
|
-
result =
|
|
122
|
+
result = self._cond(*args, **kwargs)
|
|
224
123
|
_debug("Cond %s -> %s", self.action, result)
|
|
225
124
|
return result
|
|
226
125
|
|
|
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
126
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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)}")
|
|
127
|
+
def create_action_callable(action: Action, evaluator: Evaluator) -> Callable:
|
|
128
|
+
match action:
|
|
129
|
+
case RaiseAction():
|
|
130
|
+
return create_raise_action_callable(action)
|
|
131
|
+
case AssignAction():
|
|
132
|
+
return Assign(action, evaluator)
|
|
133
|
+
case LogAction():
|
|
134
|
+
return Log(action, evaluator)
|
|
135
|
+
case IfAction():
|
|
136
|
+
return create_if_action_callable(action, evaluator)
|
|
137
|
+
case ForeachAction():
|
|
138
|
+
return create_foreach_action_callable(action, evaluator)
|
|
139
|
+
case SendAction():
|
|
140
|
+
return create_send_action_callable(action, evaluator)
|
|
141
|
+
case CancelAction():
|
|
142
|
+
return create_cancel_action_callable(action, evaluator)
|
|
143
|
+
case ScriptAction():
|
|
144
|
+
return create_script_action_callable(action, evaluator)
|
|
145
|
+
case _:
|
|
146
|
+
raise ValueError(f"Unknown action type: {type(action)}")
|
|
272
147
|
|
|
273
148
|
|
|
274
149
|
class Assign(CallableAction):
|
|
275
|
-
def __init__(self, action: AssignAction):
|
|
150
|
+
def __init__(self, action: AssignAction, evaluator: Evaluator):
|
|
276
151
|
super().__init__()
|
|
277
152
|
self.action = action
|
|
153
|
+
self._expr = (
|
|
154
|
+
evaluator.compile_value(action.expr)
|
|
155
|
+
if action.child_xml is None and action.expr is not None
|
|
156
|
+
else None
|
|
157
|
+
)
|
|
278
158
|
|
|
279
159
|
def __call__(self, *args, **kwargs):
|
|
280
160
|
machine: StateChart = kwargs["machine"]
|
|
281
161
|
if self.action.child_xml is not None:
|
|
282
162
|
value = self.action.child_xml
|
|
283
163
|
else:
|
|
284
|
-
value =
|
|
164
|
+
value = self._expr(*args, **kwargs) # type: ignore[misc]
|
|
285
165
|
|
|
286
166
|
*path, attr = self.action.location.split(".")
|
|
287
167
|
obj = machine.model
|
|
@@ -303,12 +183,13 @@ class Assign(CallableAction):
|
|
|
303
183
|
|
|
304
184
|
|
|
305
185
|
class Log(CallableAction):
|
|
306
|
-
def __init__(self, action: LogAction):
|
|
186
|
+
def __init__(self, action: LogAction, evaluator: Evaluator):
|
|
307
187
|
super().__init__()
|
|
308
188
|
self.action = action
|
|
189
|
+
self._expr = evaluator.compile_value(action.expr) if action.expr else None
|
|
309
190
|
|
|
310
191
|
def __call__(self, *args, **kwargs):
|
|
311
|
-
value =
|
|
192
|
+
value = self._expr(*args, **kwargs) if self._expr else None
|
|
312
193
|
|
|
313
194
|
if self.action.label and self.action.expr is not None:
|
|
314
195
|
msg = f"{self.action.label}: {value!r}"
|
|
@@ -319,11 +200,11 @@ class Log(CallableAction):
|
|
|
319
200
|
print(msg)
|
|
320
201
|
|
|
321
202
|
|
|
322
|
-
def create_if_action_callable(action: IfAction) -> Callable:
|
|
203
|
+
def create_if_action_callable(action: IfAction, evaluator: Evaluator) -> Callable:
|
|
323
204
|
branches = [
|
|
324
205
|
(
|
|
325
|
-
Cond.create(branch.cond),
|
|
326
|
-
[create_action_callable(action) for action in branch.actions],
|
|
206
|
+
Cond.create(branch.cond, evaluator),
|
|
207
|
+
[create_action_callable(action, evaluator) for action in branch.actions],
|
|
327
208
|
)
|
|
328
209
|
for branch in action.branches
|
|
329
210
|
]
|
|
@@ -349,14 +230,15 @@ def create_if_action_callable(action: IfAction) -> Callable:
|
|
|
349
230
|
return if_action
|
|
350
231
|
|
|
351
232
|
|
|
352
|
-
def create_foreach_action_callable(action: ForeachAction) -> Callable:
|
|
353
|
-
child_actions = [create_action_callable(act) for act in action.content.actions]
|
|
233
|
+
def create_foreach_action_callable(action: ForeachAction, evaluator: Evaluator) -> Callable:
|
|
234
|
+
child_actions = [create_action_callable(act, evaluator) for act in action.content.actions]
|
|
235
|
+
array_expr = evaluator.compile_value(action.array)
|
|
354
236
|
|
|
355
237
|
def foreach_action(*args, **kwargs):
|
|
356
238
|
machine: StateChart = kwargs["machine"]
|
|
357
239
|
try:
|
|
358
240
|
# Evaluate the array expression to get the iterable
|
|
359
|
-
array =
|
|
241
|
+
array = array_expr(*args, **kwargs)
|
|
360
242
|
except Exception as e:
|
|
361
243
|
raise ValueError(f"Error evaluating <foreach> 'array' expression: {e}") from e
|
|
362
244
|
|
|
@@ -388,17 +270,10 @@ def create_raise_action_callable(action: RaiseAction) -> Callable:
|
|
|
388
270
|
return raise_action
|
|
389
271
|
|
|
390
272
|
|
|
391
|
-
def
|
|
392
|
-
"""
|
|
273
|
+
def _resolve_event_and_params(action: SendAction, evaluator: Evaluator, **kwargs):
|
|
274
|
+
"""Evaluate the event name, namelist and params for a <send> at call time."""
|
|
393
275
|
machine = kwargs["machine"]
|
|
394
|
-
|
|
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]
|
|
276
|
+
event = action.event or evaluator.compile_value(action.eventexpr)(**kwargs) # type: ignore[arg-type]
|
|
402
277
|
names = []
|
|
403
278
|
for name in (action.namelist or "").strip().split():
|
|
404
279
|
if not hasattr(machine.model, name):
|
|
@@ -408,41 +283,46 @@ def _send_to_parent(action: SendAction, **kwargs):
|
|
|
408
283
|
for param in chain(names, action.params):
|
|
409
284
|
if param.expr is None:
|
|
410
285
|
continue
|
|
411
|
-
params_values[param.name] =
|
|
286
|
+
params_values[param.name] = evaluator.compile_value(param.expr)(**kwargs)
|
|
287
|
+
return event, params_values
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _send_to_parent(action: SendAction, evaluator: Evaluator, **kwargs):
|
|
291
|
+
"""Route a <send target="#_parent"> to the parent machine via _invoke_session."""
|
|
292
|
+
machine = kwargs["machine"]
|
|
293
|
+
session = getattr(machine, "_invoke_session", None)
|
|
294
|
+
if session is None:
|
|
295
|
+
logger.warning(
|
|
296
|
+
"<send target='#_parent'> ignored: machine %r has no _invoke_session",
|
|
297
|
+
machine.name,
|
|
298
|
+
)
|
|
299
|
+
return
|
|
300
|
+
event, params_values = _resolve_event_and_params(action, evaluator, **kwargs)
|
|
412
301
|
session.send_to_parent(event, **params_values)
|
|
413
302
|
|
|
414
303
|
|
|
415
|
-
def _send_to_invoke(action: SendAction, invokeid: str, **kwargs):
|
|
304
|
+
def _send_to_invoke(action: SendAction, invokeid: str, evaluator: Evaluator, **kwargs):
|
|
416
305
|
"""Route a <send target="#_<invokeid>"> to the invoked child session."""
|
|
417
306
|
machine: StateChart = kwargs["machine"]
|
|
418
|
-
event =
|
|
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)
|
|
307
|
+
event, params_values = _resolve_event_and_params(action, evaluator, **kwargs)
|
|
429
308
|
if not machine._engine._invoke_manager.send_to_child(invokeid, event, **params_values):
|
|
430
309
|
# Per SCXML spec: if target is not reachable → error.communication
|
|
431
310
|
BoundEvent("error.communication", internal=True, _sm=machine).put()
|
|
432
311
|
|
|
433
312
|
|
|
434
|
-
def create_send_action_callable(
|
|
313
|
+
def create_send_action_callable( # noqa: C901
|
|
314
|
+
action: SendAction, evaluator: Evaluator
|
|
315
|
+
) -> Callable:
|
|
435
316
|
content: Any = ()
|
|
436
317
|
_valid_targets = (None, "#_internal", "internal", "#_parent", "parent")
|
|
437
318
|
if action.content:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
319
|
+
content = (evaluator.eval_literal(action.content),)
|
|
320
|
+
delay_expr = (
|
|
321
|
+
evaluator.compile_value(ParseTime.replace(action.delayexpr)) if action.delayexpr else None
|
|
322
|
+
)
|
|
442
323
|
|
|
443
324
|
def send_action(*args, **kwargs): # noqa: C901
|
|
444
325
|
machine: StateChart = kwargs["machine"]
|
|
445
|
-
event = action.event or _eval(action.eventexpr, **kwargs) # type: ignore[arg-type]
|
|
446
326
|
target = action.target if action.target else None
|
|
447
327
|
|
|
448
328
|
if action.type and action.type != "http://www.w3.org/TR/scxml/#SCXMLEventProcessor":
|
|
@@ -457,7 +337,7 @@ def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
|
|
|
457
337
|
BoundEvent("error.communication", internal=True, _sm=machine).put()
|
|
458
338
|
elif target and target.startswith("#_"):
|
|
459
339
|
# #_<invokeid> → route to invoked child session
|
|
460
|
-
_send_to_invoke(action, target[2:], **kwargs)
|
|
340
|
+
_send_to_invoke(action, target[2:], evaluator, **kwargs)
|
|
461
341
|
else:
|
|
462
342
|
# Invalid target expression → error.execution (raised as exception)
|
|
463
343
|
raise ValueError(f"Invalid target: {target}. Must be one of {_valid_targets}")
|
|
@@ -465,7 +345,7 @@ def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
|
|
|
465
345
|
|
|
466
346
|
# Handle #_parent target — route to parent via _invoke_session
|
|
467
347
|
if target == "#_parent":
|
|
468
|
-
_send_to_parent(action, **kwargs)
|
|
348
|
+
_send_to_parent(action, evaluator, **kwargs)
|
|
469
349
|
return
|
|
470
350
|
|
|
471
351
|
internal = target in ("#_internal", "internal")
|
|
@@ -477,20 +357,15 @@ def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
|
|
|
477
357
|
send_id = uuid4().hex
|
|
478
358
|
setattr(machine.model, action.idlocation, send_id)
|
|
479
359
|
|
|
480
|
-
delay =
|
|
360
|
+
delay = 0.0
|
|
361
|
+
if action.delay:
|
|
362
|
+
delay = ParseTime.time_in_ms(action.delay)
|
|
363
|
+
elif delay_expr is not None:
|
|
364
|
+
delay = ParseTime.time_in_ms(delay_expr(**kwargs))
|
|
481
365
|
|
|
482
366
|
# Per SCXML spec, if namelist evaluation causes an error (e.g., variable not found),
|
|
483
367
|
# the send MUST NOT be dispatched and error.execution is raised.
|
|
484
|
-
|
|
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)
|
|
368
|
+
event, params_values = _resolve_event_and_params(action, evaluator, **kwargs)
|
|
494
369
|
|
|
495
370
|
Event(id=event, delay=delay, internal=internal, _sm=machine).put(
|
|
496
371
|
*content,
|
|
@@ -502,13 +377,15 @@ def create_send_action_callable(action: SendAction) -> Callable: # noqa: C901
|
|
|
502
377
|
return send_action
|
|
503
378
|
|
|
504
379
|
|
|
505
|
-
def create_cancel_action_callable(action: CancelAction) -> Callable:
|
|
380
|
+
def create_cancel_action_callable(action: CancelAction, evaluator: Evaluator) -> Callable:
|
|
381
|
+
sendidexpr = evaluator.compile_value(action.sendidexpr) if action.sendidexpr else None
|
|
382
|
+
|
|
506
383
|
def cancel_action(*args, **kwargs):
|
|
507
384
|
machine: StateChart = kwargs["machine"]
|
|
508
385
|
if action.sendid:
|
|
509
386
|
send_id = action.sendid
|
|
510
|
-
elif
|
|
511
|
-
send_id =
|
|
387
|
+
elif sendidexpr is not None:
|
|
388
|
+
send_id = sendidexpr(*args, **kwargs)
|
|
512
389
|
else:
|
|
513
390
|
raise ValueError("CancelAction must have either 'sendid' or 'sendidexpr'")
|
|
514
391
|
# Implement cancel logic if necessary
|
|
@@ -519,47 +396,22 @@ def create_cancel_action_callable(action: CancelAction) -> Callable:
|
|
|
519
396
|
return cancel_action
|
|
520
397
|
|
|
521
398
|
|
|
522
|
-
def create_script_action_callable(action: ScriptAction) -> Callable:
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
**machine.model.__dict__,
|
|
527
|
-
}
|
|
528
|
-
exec(action.content, {}, local_vars)
|
|
399
|
+
def create_script_action_callable(action: ScriptAction, evaluator: Evaluator) -> Callable:
|
|
400
|
+
# In the restricted (default) evaluator this raises InvalidDefinition at parse
|
|
401
|
+
# time; <script> only runs under trusted=True.
|
|
402
|
+
script = evaluator.compile_script(action.content)
|
|
529
403
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
setattr(machine.model, var_name, value)
|
|
404
|
+
def script_action(*args, **kwargs):
|
|
405
|
+
script(*args, **kwargs)
|
|
533
406
|
|
|
534
407
|
script_action.action = action # type: ignore[attr-defined]
|
|
535
408
|
return script_action
|
|
536
409
|
|
|
537
410
|
|
|
538
|
-
def
|
|
539
|
-
|
|
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
|
-
|
|
411
|
+
def _create_dataitem_callable(action: DataItem, evaluator: Evaluator) -> Callable:
|
|
412
|
+
expr_fn = evaluator.compile_value(action.expr) if action.expr else None
|
|
413
|
+
content_fn = evaluator.compile_value(action.content) if action.content else None
|
|
561
414
|
|
|
562
|
-
def _create_dataitem_callable(action: DataItem) -> Callable:
|
|
563
415
|
def data_initializer(**kwargs):
|
|
564
416
|
machine: StateChart = kwargs["machine"]
|
|
565
417
|
|
|
@@ -569,16 +421,16 @@ def _create_dataitem_callable(action: DataItem) -> Callable:
|
|
|
569
421
|
setattr(machine.model, action.id, invoke_params[action.id])
|
|
570
422
|
return
|
|
571
423
|
|
|
572
|
-
if
|
|
424
|
+
if expr_fn is not None:
|
|
573
425
|
try:
|
|
574
|
-
value =
|
|
426
|
+
value = expr_fn(**kwargs)
|
|
575
427
|
except Exception:
|
|
576
428
|
setattr(machine.model, action.id, None)
|
|
577
429
|
raise
|
|
578
430
|
|
|
579
|
-
elif
|
|
431
|
+
elif content_fn is not None:
|
|
580
432
|
try:
|
|
581
|
-
value =
|
|
433
|
+
value = content_fn(**kwargs)
|
|
582
434
|
except Exception:
|
|
583
435
|
value = action.content
|
|
584
436
|
else:
|
|
@@ -589,9 +441,11 @@ def _create_dataitem_callable(action: DataItem) -> Callable:
|
|
|
589
441
|
return data_initializer
|
|
590
442
|
|
|
591
443
|
|
|
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(
|
|
444
|
+
def create_datamodel_action_callable(action: DataModel, evaluator: Evaluator) -> "Callable | None":
|
|
445
|
+
data_elements = [_create_dataitem_callable(item, evaluator) for item in action.data]
|
|
446
|
+
data_elements.extend(
|
|
447
|
+
[create_script_action_callable(script, evaluator) for script in action.scripts]
|
|
448
|
+
)
|
|
595
449
|
|
|
596
450
|
if not data_elements:
|
|
597
451
|
return None
|
|
@@ -613,10 +467,12 @@ def create_datamodel_action_callable(action: DataModel) -> "Callable | None":
|
|
|
613
467
|
class ExecuteBlock(CallableAction):
|
|
614
468
|
"""Parses the children as <executable> content XML into a callable."""
|
|
615
469
|
|
|
616
|
-
def __init__(self, content: ExecutableContent):
|
|
470
|
+
def __init__(self, content: ExecutableContent, evaluator: Evaluator):
|
|
617
471
|
super().__init__()
|
|
618
472
|
self.action = content
|
|
619
|
-
self.action_callables = [
|
|
473
|
+
self.action_callables = [
|
|
474
|
+
create_action_callable(action, evaluator) for action in content.actions
|
|
475
|
+
]
|
|
620
476
|
|
|
621
477
|
def __call__(self, *args, **kwargs):
|
|
622
478
|
for action in self.action_callables:
|
|
@@ -626,25 +482,39 @@ class ExecuteBlock(CallableAction):
|
|
|
626
482
|
class DoneDataCallable(CallableAction):
|
|
627
483
|
"""Evaluates <donedata> params/content and returns the data for done events."""
|
|
628
484
|
|
|
629
|
-
def __init__(self, donedata: DoneData):
|
|
485
|
+
def __init__(self, donedata: DoneData, evaluator: Evaluator):
|
|
630
486
|
super().__init__()
|
|
631
487
|
self.action = donedata
|
|
632
488
|
self.donedata = donedata
|
|
489
|
+
self._content_expr = (
|
|
490
|
+
evaluator.compile_value(donedata.content_expr)
|
|
491
|
+
if donedata.content_expr is not None
|
|
492
|
+
else None
|
|
493
|
+
)
|
|
494
|
+
self._params = [
|
|
495
|
+
(
|
|
496
|
+
param.name,
|
|
497
|
+
evaluator.compile_value(param.expr)
|
|
498
|
+
if param.expr is not None
|
|
499
|
+
else evaluator.compile_value(param.location.strip()), # type: ignore[union-attr]
|
|
500
|
+
param.location,
|
|
501
|
+
)
|
|
502
|
+
for param in donedata.params
|
|
503
|
+
]
|
|
633
504
|
|
|
634
505
|
def __call__(self, *args, **kwargs):
|
|
635
|
-
if self.
|
|
636
|
-
return
|
|
506
|
+
if self._content_expr is not None:
|
|
507
|
+
return self._content_expr(*args, **kwargs)
|
|
637
508
|
|
|
638
509
|
result = {}
|
|
639
|
-
for
|
|
640
|
-
if
|
|
641
|
-
result[
|
|
642
|
-
|
|
643
|
-
location = param.location.strip()
|
|
510
|
+
for name, fn, location in self._params:
|
|
511
|
+
if location is None:
|
|
512
|
+
result[name] = fn(*args, **kwargs)
|
|
513
|
+
else:
|
|
644
514
|
try:
|
|
645
|
-
result[
|
|
515
|
+
result[name] = fn(*args, **kwargs)
|
|
646
516
|
except Exception as e:
|
|
647
517
|
raise ValueError(
|
|
648
|
-
f"<param> location '{location}' does not resolve to a valid value"
|
|
518
|
+
f"<param> location '{location.strip()}' does not resolve to a valid value"
|
|
649
519
|
) from e
|
|
650
520
|
return result
|