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/spec_parser.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
import operator
|
|
3
3
|
import re
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from functools import reduce
|
|
5
6
|
from inspect import isawaitable
|
|
6
|
-
from typing import Callable
|
|
7
|
-
from typing import Dict
|
|
8
7
|
|
|
9
8
|
replacements = {"!": "not ", "^": " and ", "v": " or "}
|
|
10
9
|
|
|
@@ -20,6 +19,15 @@ comparison_repr = {
|
|
|
20
19
|
}
|
|
21
20
|
|
|
22
21
|
|
|
22
|
+
class UnsupportedExpression(ValueError):
|
|
23
|
+
"""The expression contains a structure that is not on the parser allowlist.
|
|
24
|
+
|
|
25
|
+
Distinguishes a rejected expression from errors raised by the ``variable_hook``
|
|
26
|
+
while resolving names, so callers can report each one properly. Inherits from
|
|
27
|
+
``ValueError`` to keep the previous behavior for callers catching it.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
23
31
|
def _unique_key(left, right, operator) -> str:
|
|
24
32
|
left_key = getattr(left, "unique_key", "")
|
|
25
33
|
right_key = getattr(right, "unique_key", "")
|
|
@@ -115,7 +123,7 @@ def build_constant(constant) -> Callable:
|
|
|
115
123
|
|
|
116
124
|
|
|
117
125
|
class Functions:
|
|
118
|
-
registry:
|
|
126
|
+
registry: dict[str, Callable] = {}
|
|
119
127
|
|
|
120
128
|
@classmethod
|
|
121
129
|
def register(cls, id) -> Callable:
|
|
@@ -129,7 +137,7 @@ class Functions:
|
|
|
129
137
|
def get(cls, func_id):
|
|
130
138
|
func_id = func_id.lower()
|
|
131
139
|
if func_id not in cls.registry:
|
|
132
|
-
raise
|
|
140
|
+
raise UnsupportedExpression(f"Unsupported function: {func_id}")
|
|
133
141
|
return cls.registry[func_id]
|
|
134
142
|
|
|
135
143
|
|
|
@@ -178,45 +186,142 @@ def build_custom_operator(operator) -> Callable:
|
|
|
178
186
|
return custom_comparator
|
|
179
187
|
|
|
180
188
|
|
|
181
|
-
def
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
189
|
+
def build_binop(op_fn, left: Callable, right: Callable) -> Callable:
|
|
190
|
+
def decorated(*args, **kwargs):
|
|
191
|
+
return op_fn(left(*args, **kwargs), right(*args, **kwargs))
|
|
192
|
+
|
|
193
|
+
decorated.__name__ = f"({left.__name__} {op_fn.__name__} {right.__name__})"
|
|
194
|
+
return decorated
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def build_unaryop(op_fn, operand: Callable) -> Callable:
|
|
198
|
+
def decorated(*args, **kwargs):
|
|
199
|
+
return op_fn(operand(*args, **kwargs))
|
|
200
|
+
|
|
201
|
+
decorated.__name__ = f"{op_fn.__name__}({operand.__name__})"
|
|
202
|
+
return decorated
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def build_collection(factory, item_exprs: "list[Callable]") -> Callable:
|
|
206
|
+
def decorated(*args, **kwargs):
|
|
207
|
+
return factory(item(*args, **kwargs) for item in item_exprs)
|
|
208
|
+
|
|
209
|
+
decorated.__name__ = f"{factory.__name__}(...)"
|
|
210
|
+
return decorated
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def build_dict(key_exprs: "list[Callable]", value_exprs: "list[Callable]") -> Callable:
|
|
214
|
+
def decorated(*args, **kwargs):
|
|
215
|
+
return {
|
|
216
|
+
key(*args, **kwargs): value(*args, **kwargs)
|
|
217
|
+
for key, value in zip(key_exprs, value_exprs, strict=True)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
decorated.__name__ = "dict(...)"
|
|
221
|
+
return decorated
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def build_subscript(value_expr: Callable, slice_expr: Callable) -> Callable:
|
|
225
|
+
def decorated(*args, **kwargs):
|
|
226
|
+
return value_expr(*args, **kwargs)[slice_expr(*args, **kwargs)]
|
|
227
|
+
|
|
228
|
+
decorated.__name__ = f"{value_expr.__name__}[{slice_expr.__name__}]"
|
|
229
|
+
return decorated
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_attribute(value_expr: Callable, attr: str) -> Callable:
|
|
233
|
+
def decorated(*args, **kwargs):
|
|
234
|
+
return getattr(value_expr(*args, **kwargs), attr)
|
|
235
|
+
|
|
236
|
+
decorated.__name__ = f"{value_expr.__name__}.{attr}"
|
|
237
|
+
return decorated
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def build_expression( # noqa: C901
|
|
241
|
+
node, variable_hook, operator_mapping, allow_value_nodes: bool = False
|
|
242
|
+
):
|
|
243
|
+
"""Build a callable from an AST node using an allowlist of allowed structures.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
allow_value_nodes: when ``True``, value-producing structures (arithmetic,
|
|
247
|
+
collections, subscript, attribute read) are also accepted. The DSL
|
|
248
|
+
boolean-guard parser keeps this ``False`` so non-boolean expressions
|
|
249
|
+
(e.g. ``a * b``, ``{}``) remain rejected; the SCXML datamodel parser
|
|
250
|
+
(:func:`parse_expr`) sets it ``True``.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def recurse(child):
|
|
254
|
+
return build_expression(child, variable_hook, operator_mapping, allow_value_nodes)
|
|
255
|
+
|
|
256
|
+
match node:
|
|
257
|
+
case ast.BoolOp():
|
|
258
|
+
# `and` / `or` operations
|
|
259
|
+
operator_fn = operator_mapping[type(node.op)]
|
|
260
|
+
left_expr = recurse(node.values[0])
|
|
261
|
+
for right in node.values[1:]:
|
|
262
|
+
right_expr = recurse(right)
|
|
263
|
+
left_expr = operator_fn(left_expr, right_expr)
|
|
264
|
+
return left_expr
|
|
265
|
+
case ast.Compare():
|
|
266
|
+
# `==` / `!=` / `>` / `<` / `>=` / `<=` operations
|
|
267
|
+
expressions = []
|
|
268
|
+
left_expr = recurse(node.left)
|
|
269
|
+
for right_op, right in zip(node.ops, node.comparators, strict=True):
|
|
270
|
+
right_expr = recurse(right)
|
|
271
|
+
operator_fn = operator_mapping[type(right_op)]
|
|
272
|
+
expression = operator_fn(left_expr, right_expr)
|
|
273
|
+
left_expr = right_expr
|
|
274
|
+
expressions.append(expression)
|
|
275
|
+
return reduce(custom_and, expressions)
|
|
276
|
+
case ast.Call(func=ast.Name(id=func_id)):
|
|
277
|
+
# Only allowlisted functions from the registry (e.g. ``In(...)``) are
|
|
278
|
+
# callable. Method calls (``obj.method()``) have an ``ast.Attribute``
|
|
279
|
+
# func and fall through to the ``case _`` guard below — this prevents
|
|
280
|
+
# using calls as a sandbox-escape vector.
|
|
281
|
+
constructor = Functions.get(func_id)
|
|
282
|
+
params = [arg.value for arg in node.args if isinstance(arg, ast.Constant)]
|
|
283
|
+
return constructor(*params)
|
|
284
|
+
case ast.UnaryOp(op=ast.Not()):
|
|
285
|
+
return operator_mapping[type(node.op)](recurse(node.operand))
|
|
286
|
+
case ast.UnaryOp(op=(ast.USub() | ast.UAdd())) if allow_value_nodes:
|
|
287
|
+
return build_unaryop(unary_operators[type(node.op)], recurse(node.operand))
|
|
288
|
+
case ast.BinOp() if allow_value_nodes:
|
|
289
|
+
op_type = type(node.op)
|
|
290
|
+
if op_type not in binary_operators:
|
|
291
|
+
# e.g. bitwise ``^``/``|``/``<<`` are outside the allowlist. (``**`` and ``*``
|
|
292
|
+
# are allowed but magnitude-capped — see ``binary_operators``.)
|
|
293
|
+
raise ValueError(f"Binary operator '{op_type.__name__}' is not allowed")
|
|
294
|
+
return build_binop(binary_operators[op_type], recurse(node.left), recurse(node.right))
|
|
295
|
+
case ast.List(elts=elts) if allow_value_nodes:
|
|
296
|
+
return build_collection(list, [recurse(e) for e in elts])
|
|
297
|
+
case ast.Tuple(elts=elts) if allow_value_nodes:
|
|
298
|
+
return build_collection(tuple, [recurse(e) for e in elts])
|
|
299
|
+
case ast.Set(elts=elts) if allow_value_nodes:
|
|
300
|
+
return build_collection(set, [recurse(e) for e in elts])
|
|
301
|
+
case ast.Dict(keys=keys, values=values) if allow_value_nodes and all(
|
|
302
|
+
key is not None for key in keys
|
|
303
|
+
):
|
|
304
|
+
# ``key is not None`` rejects dict unpacking (``{**other}``), whose key
|
|
305
|
+
# node is ``None``.
|
|
306
|
+
return build_dict([recurse(key) for key in keys], [recurse(value) for value in values])
|
|
307
|
+
case ast.Subscript() if allow_value_nodes:
|
|
308
|
+
return build_subscript(recurse(node.value), recurse(node.slice))
|
|
309
|
+
case ast.Attribute(attr=attr) if allow_value_nodes:
|
|
310
|
+
# Block dunder/private attribute access (``__class__``, ``__globals__``,
|
|
311
|
+
# ...), the classic sandbox-escape chain. Subscript is allowed because,
|
|
312
|
+
# without underscore-attribute access or method calls, it cannot reach
|
|
313
|
+
# type objects.
|
|
314
|
+
if attr.startswith("_"):
|
|
315
|
+
raise UnsupportedExpression(f"Attribute access to '{attr}' is not allowed")
|
|
316
|
+
return build_attribute(recurse(node.value), attr)
|
|
317
|
+
case ast.Name(id=name):
|
|
318
|
+
return variable_hook(name)
|
|
319
|
+
case ast.Constant(value=value):
|
|
320
|
+
return build_constant(value)
|
|
321
|
+
case _:
|
|
322
|
+
raise UnsupportedExpression(
|
|
323
|
+
f"Unsupported expression structure: {node.__class__.__name__}"
|
|
324
|
+
)
|
|
220
325
|
|
|
221
326
|
|
|
222
327
|
def parse_boolean_expr(expr, variable_hook, operator_mapping):
|
|
@@ -224,14 +329,39 @@ def parse_boolean_expr(expr, variable_hook, operator_mapping):
|
|
|
224
329
|
if expr.strip() == "":
|
|
225
330
|
raise SyntaxError("Empty expression")
|
|
226
331
|
|
|
227
|
-
# Optimization
|
|
228
|
-
|
|
332
|
+
# Optimization: a lone identifier can only be a variable name, so there is
|
|
333
|
+
# nothing to parse. Anything else (operators, comparisons, spaces, calls)
|
|
334
|
+
# goes through the parser.
|
|
335
|
+
if expr.isidentifier():
|
|
229
336
|
return variable_hook(expr)
|
|
230
337
|
expr = replace_operators(expr)
|
|
231
338
|
tree = ast.parse(expr, mode="eval")
|
|
232
339
|
return build_expression(tree.body, variable_hook, operator_mapping)
|
|
233
340
|
|
|
234
341
|
|
|
342
|
+
def parse_expr(expr: str, variable_hook: Callable) -> Callable:
|
|
343
|
+
"""Parse a value expression into a callable using the restricted AST allowlist.
|
|
344
|
+
|
|
345
|
+
Unlike :func:`parse_boolean_expr`, this does not apply the DSL operator
|
|
346
|
+
replacement (``!``/``^``/``v``) and does not coerce the top-level result to
|
|
347
|
+
``bool`` — it returns the raw evaluated value. Used to safely evaluate SCXML
|
|
348
|
+
datamodel expressions (``<assign>``, ``<send>``, ``<foreach>``, ``<data>``)
|
|
349
|
+
without :func:`eval`.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
ValueError: if the expression uses a structure outside the allowlist
|
|
353
|
+
(e.g. attribute access to dunders, method calls, lambdas).
|
|
354
|
+
SyntaxError: if the expression is empty or not valid Python.
|
|
355
|
+
"""
|
|
356
|
+
if expr.strip() == "":
|
|
357
|
+
raise SyntaxError("Empty expression")
|
|
358
|
+
tree = ast.parse(expr, mode="eval")
|
|
359
|
+
compiled: Callable = build_expression(
|
|
360
|
+
tree.body, variable_hook, operator_mapping, allow_value_nodes=True
|
|
361
|
+
)
|
|
362
|
+
return compiled
|
|
363
|
+
|
|
364
|
+
|
|
235
365
|
operator_mapping = {
|
|
236
366
|
ast.Or: custom_or,
|
|
237
367
|
ast.And: custom_and,
|
|
@@ -243,3 +373,56 @@ operator_mapping = {
|
|
|
243
373
|
ast.Eq: build_custom_operator(operator.eq),
|
|
244
374
|
ast.NotEq: build_custom_operator(operator.ne),
|
|
245
375
|
}
|
|
376
|
+
|
|
377
|
+
# Result-size caps for the two operators that can blow up cheaply. Ordinary scalar
|
|
378
|
+
# arithmetic (``x * 2``, ``x ** 2``) is well under these; the caps only reject the
|
|
379
|
+
# denial-of-service forms (``9**9**9`` bignum, ``[0]*20000000`` sequence replication).
|
|
380
|
+
_MAX_POW_RESULT_BITS = 4096
|
|
381
|
+
_MAX_SEQUENCE_LEN = 1_000_000
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _guarded_pow(base, exp):
|
|
385
|
+
"""``**`` with a magnitude cap (GHSA-r8gj-366q-cgvj).
|
|
386
|
+
|
|
387
|
+
``int ** int`` can allocate a giant bignum from a tiny expression (``9**9**9`` is a
|
|
388
|
+
~370-million-digit number). The result size is estimated *before* computing, so the
|
|
389
|
+
allocation never happens. Non-integer operands (floats overflow to ``inf`` instead of
|
|
390
|
+
growing without bound) are passed straight through.
|
|
391
|
+
"""
|
|
392
|
+
if isinstance(base, int) and isinstance(exp, int) and exp > 0 and base not in (0, 1, -1):
|
|
393
|
+
if base.bit_length() * exp > _MAX_POW_RESULT_BITS:
|
|
394
|
+
raise ValueError("'**' result is too large for the restricted evaluator")
|
|
395
|
+
return operator.pow(base, exp)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _guarded_mul(a, b):
|
|
399
|
+
"""``*`` with a sequence-replication cap (GHSA-r8gj-366q-cgvj).
|
|
400
|
+
|
|
401
|
+
``seq * n`` (list/str/bytes/tuple times an int) can allocate an enormous object from a
|
|
402
|
+
12-character expression (``[0]*20000000``). The resulting length is checked *before*
|
|
403
|
+
allocating. Scalar numeric multiplication is unaffected.
|
|
404
|
+
"""
|
|
405
|
+
for seq, n in ((a, b), (b, a)):
|
|
406
|
+
if isinstance(seq, (str, bytes, bytearray, list, tuple)) and isinstance(n, int):
|
|
407
|
+
if n > 0 and n * len(seq) > _MAX_SEQUENCE_LEN:
|
|
408
|
+
raise ValueError("'*' repetition is too large for the restricted evaluator")
|
|
409
|
+
return operator.mul(a, b)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
# ``**`` and ``*`` stay available for ordinary arithmetic but are wrapped so a tiny untrusted
|
|
413
|
+
# expression cannot exhaust CPU/memory (GHSA-r8gj-366q-cgvj). ``trusted=True`` uses the full
|
|
414
|
+
# Python evaluator instead, without these caps.
|
|
415
|
+
binary_operators = {
|
|
416
|
+
ast.Add: operator.add,
|
|
417
|
+
ast.Sub: operator.sub,
|
|
418
|
+
ast.Mult: _guarded_mul,
|
|
419
|
+
ast.Div: operator.truediv,
|
|
420
|
+
ast.FloorDiv: operator.floordiv,
|
|
421
|
+
ast.Mod: operator.mod,
|
|
422
|
+
ast.Pow: _guarded_pow,
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
unary_operators = {
|
|
426
|
+
ast.USub: operator.neg,
|
|
427
|
+
ast.UAdd: operator.pos,
|
|
428
|
+
}
|
statemachine/state.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from enum import Enum
|
|
2
3
|
from typing import TYPE_CHECKING
|
|
3
4
|
from typing import Any
|
|
4
|
-
from typing import Generator
|
|
5
|
-
from typing import List
|
|
6
5
|
from typing import cast
|
|
7
6
|
from weakref import ref
|
|
8
7
|
|
|
@@ -214,8 +213,8 @@ class State:
|
|
|
214
213
|
initial: bool = False,
|
|
215
214
|
final: bool = False,
|
|
216
215
|
parallel: bool = False,
|
|
217
|
-
states: "
|
|
218
|
-
history: "
|
|
216
|
+
states: "list[State] | None" = None,
|
|
217
|
+
history: "list[HistoryState] | None" = None,
|
|
219
218
|
enter: Any = None,
|
|
220
219
|
exit: Any = None,
|
|
221
220
|
invoke: Any = None,
|
|
@@ -284,7 +283,7 @@ class State:
|
|
|
284
283
|
f"on_invoke_{self.id}", priority=CallbackPriority.NAMING, is_convention=True
|
|
285
284
|
)
|
|
286
285
|
|
|
287
|
-
def _on_event_defined(self, event: str, transition: Transition, states:
|
|
286
|
+
def _on_event_defined(self, event: str, transition: Transition, states: list["State"]):
|
|
288
287
|
"""Called by statemachine factory when an event is defined having a transition
|
|
289
288
|
starting from this state.
|
|
290
289
|
"""
|
|
@@ -297,7 +296,7 @@ class State:
|
|
|
297
296
|
)
|
|
298
297
|
|
|
299
298
|
def __str__(self):
|
|
300
|
-
return self.name
|
|
299
|
+
return str(self.name)
|
|
301
300
|
|
|
302
301
|
@property
|
|
303
302
|
def id(self) -> str:
|
|
@@ -309,7 +308,7 @@ class State:
|
|
|
309
308
|
self.value = id
|
|
310
309
|
if not self.name:
|
|
311
310
|
self.name = humanize_id(self._id)
|
|
312
|
-
self._hash = hash((self.name, self._id))
|
|
311
|
+
self._hash = hash((str(self.name), self._id))
|
|
313
312
|
|
|
314
313
|
return self
|
|
315
314
|
|
|
@@ -417,7 +416,7 @@ class AnyState(State):
|
|
|
417
416
|
until the state machine class is evaluated.
|
|
418
417
|
"""
|
|
419
418
|
|
|
420
|
-
def _on_event_defined(self, event: str, transition: Transition, states:
|
|
419
|
+
def _on_event_defined(self, event: str, transition: Transition, states: list[State]):
|
|
421
420
|
for state in states:
|
|
422
421
|
if state.final:
|
|
423
422
|
continue
|
statemachine/statemachine.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import warnings
|
|
2
|
+
from collections.abc import MutableSet
|
|
2
3
|
from inspect import isawaitable
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Any
|
|
5
|
-
from typing import Dict
|
|
6
6
|
from typing import Generic
|
|
7
|
-
from typing import List
|
|
8
|
-
from typing import MutableSet
|
|
9
7
|
from typing import TypeVar
|
|
10
8
|
|
|
11
9
|
from statemachine.orderedset import OrderedSet
|
|
@@ -106,7 +104,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
106
104
|
produce an ``error.execution`` internal event instead of propagating, as mandated by the
|
|
107
105
|
SCXML specification. If ``False``, exceptions propagate normally."""
|
|
108
106
|
|
|
109
|
-
start_configuration_values:
|
|
107
|
+
start_configuration_values: list[Any] = []
|
|
110
108
|
"""Default state values to be entered when the state machine starts.
|
|
111
109
|
|
|
112
110
|
If empty (default), the root ``initial`` state will be used.
|
|
@@ -123,21 +121,21 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
123
121
|
states: "States"
|
|
124
122
|
"""Collection of top-level :ref:`State` objects declared on this class."""
|
|
125
123
|
|
|
126
|
-
states_map:
|
|
124
|
+
states_map: dict[Any, "State"]
|
|
127
125
|
"""Mapping from each state's ``value`` to the corresponding :ref:`State` instance.
|
|
128
126
|
Includes states at all nesting levels (compound children, parallel regions, etc.)."""
|
|
129
127
|
|
|
130
128
|
initial_state: "State | None"
|
|
131
129
|
"""The single top-level initial :ref:`State`, or ``None`` for abstract classes."""
|
|
132
130
|
|
|
133
|
-
final_states: "
|
|
131
|
+
final_states: "list[State]"
|
|
134
132
|
"""List of top-level :ref:`State` objects marked as ``final``."""
|
|
135
133
|
|
|
136
134
|
_abstract: bool
|
|
137
|
-
_events: "
|
|
135
|
+
_events: "dict[Event, None]"
|
|
138
136
|
_protected_attrs: set
|
|
139
137
|
_specs: CallbackSpecList
|
|
140
|
-
_class_listeners:
|
|
138
|
+
_class_listeners: list[Any]
|
|
141
139
|
prepare: SpecListGrouper
|
|
142
140
|
|
|
143
141
|
def __init__(
|
|
@@ -145,14 +143,14 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
145
143
|
model: "TModel | None" = None,
|
|
146
144
|
state_field: str = "state",
|
|
147
145
|
start_value: Any = None,
|
|
148
|
-
listeners: "
|
|
146
|
+
listeners: "list[object] | None" = None,
|
|
149
147
|
**kwargs: Any,
|
|
150
148
|
):
|
|
151
149
|
self.model: TModel = model if model is not None else Model() # type: ignore[assignment]
|
|
152
150
|
"""The external model object that holds domain state, or an internal
|
|
153
151
|
:class:`Model` instance when none is provided. See :ref:`domain models`."""
|
|
154
152
|
|
|
155
|
-
self.history_values:
|
|
153
|
+
self.history_values: dict[str, list[State]] = {}
|
|
156
154
|
"""Mapping from compound state IDs to the list of states that were active
|
|
157
155
|
the last time that compound state was exited. Used by history pseudo-states
|
|
158
156
|
to restore previous configurations."""
|
|
@@ -162,7 +160,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
162
160
|
)
|
|
163
161
|
self._callbacks = CallbacksRegistry()
|
|
164
162
|
self._config = self._build_configuration()
|
|
165
|
-
self._listeners:
|
|
163
|
+
self._listeners: dict[int, Any] = {}
|
|
166
164
|
"""Listeners that provides attributes to be used as callbacks."""
|
|
167
165
|
|
|
168
166
|
if self._abstract:
|
|
@@ -184,8 +182,8 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
184
182
|
|
|
185
183
|
return SyncEngine(self)
|
|
186
184
|
|
|
187
|
-
def _resolve_class_listeners(self, **kwargs: Any) ->
|
|
188
|
-
resolved:
|
|
185
|
+
def _resolve_class_listeners(self, **kwargs: Any) -> list[object]:
|
|
186
|
+
resolved: list[object] = []
|
|
189
187
|
for entry in self._class_listeners:
|
|
190
188
|
if callable(entry):
|
|
191
189
|
instance = entry()
|
|
@@ -206,11 +204,11 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
206
204
|
|
|
207
205
|
def _build_configuration(self) -> Configuration:
|
|
208
206
|
"""Create InstanceState entries and return a new Configuration."""
|
|
209
|
-
instance_states:
|
|
207
|
+
instance_states: dict[Any, Any] = {}
|
|
210
208
|
events = self.__class__._events
|
|
211
209
|
for state in self.states_map.values():
|
|
212
210
|
ist = InstanceState(state, self)
|
|
213
|
-
instance_states[state.
|
|
211
|
+
instance_states[state.value] = ist
|
|
214
212
|
if state.id not in events:
|
|
215
213
|
vars(self)[state.id] = ist
|
|
216
214
|
return Configuration(
|
|
@@ -259,7 +257,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
259
257
|
del state["_engine"]
|
|
260
258
|
return state
|
|
261
259
|
|
|
262
|
-
def __setstate__(self, state:
|
|
260
|
+
def __setstate__(self, state: dict[str, Any]) -> None:
|
|
263
261
|
listeners = state.pop("_listeners")
|
|
264
262
|
self.__dict__.update(state) # type: ignore[attr-defined]
|
|
265
263
|
self._callbacks = CallbacksRegistry()
|
|
@@ -312,7 +310,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
312
310
|
|
|
313
311
|
return self
|
|
314
312
|
|
|
315
|
-
def _register_callbacks(self, listeners:
|
|
313
|
+
def _register_callbacks(self, listeners: list[object]):
|
|
316
314
|
self._listeners.update({id(listener): listener for listener in listeners})
|
|
317
315
|
self._add_listener(
|
|
318
316
|
Listeners.from_listeners(
|
|
@@ -336,7 +334,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
336
334
|
self._callbacks.async_or_sync()
|
|
337
335
|
|
|
338
336
|
@property
|
|
339
|
-
def active_listeners(self) ->
|
|
337
|
+
def active_listeners(self) -> list[object]:
|
|
340
338
|
"""List of all active listeners attached to this instance.
|
|
341
339
|
|
|
342
340
|
Includes class-level listeners (resolved from the ``listeners`` class attribute),
|
|
@@ -423,12 +421,12 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
423
421
|
self.current_state_value = value.value
|
|
424
422
|
|
|
425
423
|
@property
|
|
426
|
-
def events(self) -> "
|
|
424
|
+
def events(self) -> "list[Event]":
|
|
427
425
|
"""List of all :ref:`Event` instances declared on this state machine."""
|
|
428
426
|
return [getattr(self, event) for event in self.__class__._events]
|
|
429
427
|
|
|
430
428
|
@property
|
|
431
|
-
def allowed_events(self) -> "
|
|
429
|
+
def allowed_events(self) -> "list[Event]":
|
|
432
430
|
"""List of the current allowed events."""
|
|
433
431
|
return [
|
|
434
432
|
getattr(self, event)
|
statemachine/states.py
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import Dict # deprecated since 3.9: https://peps.python.org/pep-0585/
|
|
3
|
-
from typing import Type
|
|
4
2
|
|
|
5
3
|
from .state import State
|
|
6
4
|
from .utils import ensure_iterable
|
|
7
5
|
|
|
8
|
-
EnumType =
|
|
6
|
+
EnumType = type[Enum]
|
|
9
7
|
|
|
10
8
|
|
|
11
9
|
class States:
|
|
@@ -35,7 +33,7 @@ class States:
|
|
|
35
33
|
|
|
36
34
|
"""
|
|
37
35
|
|
|
38
|
-
def __init__(self, states: "
|
|
36
|
+
def __init__(self, states: "dict[str, State] | None" = None) -> None:
|
|
39
37
|
"""
|
|
40
38
|
Initializes a new instance of the States class.
|
|
41
39
|
|
|
@@ -45,7 +43,7 @@ class States:
|
|
|
45
43
|
Returns:
|
|
46
44
|
None.
|
|
47
45
|
"""
|
|
48
|
-
self._states:
|
|
46
|
+
self._states: dict[str, State] = states if states is not None else {}
|
|
49
47
|
|
|
50
48
|
def __repr__(self):
|
|
51
49
|
return f"{list(self)}"
|
statemachine/transition.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from copy import deepcopy
|
|
2
2
|
from typing import TYPE_CHECKING
|
|
3
|
-
from typing import List
|
|
4
3
|
|
|
5
4
|
from .callbacks import CallbackGroup
|
|
6
5
|
from .callbacks import CallbackPriority
|
|
@@ -43,7 +42,7 @@ class Transition:
|
|
|
43
42
|
def __init__(
|
|
44
43
|
self,
|
|
45
44
|
source: "State",
|
|
46
|
-
target: "State |
|
|
45
|
+
target: "State | list[State] | None" = None,
|
|
47
46
|
event=None,
|
|
48
47
|
internal=False,
|
|
49
48
|
initial=False,
|
|
@@ -56,7 +55,7 @@ class Transition:
|
|
|
56
55
|
):
|
|
57
56
|
self.source = source
|
|
58
57
|
if isinstance(target, list):
|
|
59
|
-
self._targets: "
|
|
58
|
+
self._targets: "list[State]" = target
|
|
60
59
|
elif target is not None:
|
|
61
60
|
self._targets = [target]
|
|
62
61
|
else:
|
|
@@ -104,7 +103,7 @@ class Transition:
|
|
|
104
103
|
return self._targets[0] if self._targets else None
|
|
105
104
|
|
|
106
105
|
@property
|
|
107
|
-
def targets(self) -> "
|
|
106
|
+
def targets(self) -> "list[State]":
|
|
108
107
|
"""All target states. For single-target transitions, returns a one-element list."""
|
|
109
108
|
return self._targets
|
|
110
109
|
|
statemachine/transition_list.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
1
2
|
from typing import TYPE_CHECKING
|
|
2
|
-
from typing import Iterable
|
|
3
|
-
from typing import List
|
|
4
3
|
|
|
5
4
|
from .callbacks import CallbackGroup
|
|
6
5
|
from .transition import Transition
|
|
@@ -22,7 +21,7 @@ class TransitionList(AddCallbacksMixin):
|
|
|
22
21
|
Defaults to `None`.
|
|
23
22
|
|
|
24
23
|
"""
|
|
25
|
-
self.transitions:
|
|
24
|
+
self.transitions: list[Transition] = list(transitions) if transitions else []
|
|
26
25
|
|
|
27
26
|
def __repr__(self):
|
|
28
27
|
"""Return a string representation of the :ref:`TransitionList`."""
|
|
@@ -42,7 +41,7 @@ class TransitionList(AddCallbacksMixin):
|
|
|
42
41
|
"""
|
|
43
42
|
return TransitionList(self.transitions).add_transitions(other)
|
|
44
43
|
|
|
45
|
-
def _on_event_defined(self, event: str, states:
|
|
44
|
+
def _on_event_defined(self, event: str, states: list["State"]):
|
|
46
45
|
self.add_event(event)
|
|
47
46
|
|
|
48
47
|
for transition in self.transitions:
|
|
@@ -111,7 +110,7 @@ class TransitionList(AddCallbacksMixin):
|
|
|
111
110
|
transition.add_event(event)
|
|
112
111
|
|
|
113
112
|
@property
|
|
114
|
-
def unique_events(self) ->
|
|
113
|
+
def unique_events(self) -> list["Event"]:
|
|
115
114
|
"""
|
|
116
115
|
Returns a list of unique event names across all transitions in the :ref:`TransitionList`
|
|
117
116
|
instance.
|