python-statemachine 3.2.0__py3-none-any.whl → 3.2.2__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.2.0.dist-info → python_statemachine-3.2.2.dist-info}/METADATA +7 -6
- python_statemachine-3.2.2.dist-info/RECORD +73 -0
- {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +11 -0
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/extract.py +3 -3
- statemachine/contrib/diagram/formatter.py +4 -2
- statemachine/contrib/diagram/graphviz.py +66 -0
- statemachine/contrib/diagram/sphinx_ext.py +28 -7
- statemachine/dispatcher.py +8 -3
- statemachine/engines/async_.py +224 -180
- statemachine/engines/base.py +257 -245
- statemachine/engines/sync.py +224 -108
- statemachine/event.py +29 -10
- statemachine/event_data.py +19 -3
- statemachine/events.py +5 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +126 -29
- statemachine/io/__init__.py +18 -11
- statemachine/io/actions.py +124 -15
- statemachine/io/evaluators.py +37 -9
- statemachine/io/invoke.py +12 -0
- statemachine/io/loader.py +17 -3
- statemachine/io/schemas/statechart.schema.json +8 -1
- statemachine/io/scxml/__init__.py +6 -3
- statemachine/io/scxml/processor.py +1 -1
- statemachine/io/scxml/reader.py +24 -9
- statemachine/io/system_variables.py +34 -19
- statemachine/locale/en/LC_MESSAGES/statemachine.po +58 -17
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +60 -17
- statemachine/orderedset.py +12 -3
- statemachine/registry.py +6 -1
- statemachine/spec_parser.py +151 -21
- statemachine/state.py +47 -8
- statemachine/statemachine.py +42 -19
- python_statemachine-3.2.0.dist-info/RECORD +0 -72
- {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/licenses/LICENSE +0 -0
statemachine/spec_parser.py
CHANGED
|
@@ -19,6 +19,15 @@ comparison_repr = {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
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
|
+
|
|
22
31
|
def _unique_key(left, right, operator) -> str:
|
|
23
32
|
left_key = getattr(left, "unique_key", "")
|
|
24
33
|
right_key = getattr(right, "unique_key", "")
|
|
@@ -128,7 +137,7 @@ class Functions:
|
|
|
128
137
|
def get(cls, func_id):
|
|
129
138
|
func_id = func_id.lower()
|
|
130
139
|
if func_id not in cls.registry:
|
|
131
|
-
raise
|
|
140
|
+
raise UnsupportedExpression(f"Unsupported function: {func_id}")
|
|
132
141
|
return cls.registry[func_id]
|
|
133
142
|
|
|
134
143
|
|
|
@@ -195,7 +204,9 @@ def build_unaryop(op_fn, operand: Callable) -> Callable:
|
|
|
195
204
|
|
|
196
205
|
def build_collection(factory, item_exprs: "list[Callable]") -> Callable:
|
|
197
206
|
def decorated(*args, **kwargs):
|
|
198
|
-
|
|
207
|
+
items = [item(*args, **kwargs) for item in item_exprs]
|
|
208
|
+
_check_value_size(items, "collection")
|
|
209
|
+
return factory(items)
|
|
199
210
|
|
|
200
211
|
decorated.__name__ = f"{factory.__name__}(...)"
|
|
201
212
|
return decorated
|
|
@@ -203,10 +214,12 @@ def build_collection(factory, item_exprs: "list[Callable]") -> Callable:
|
|
|
203
214
|
|
|
204
215
|
def build_dict(key_exprs: "list[Callable]", value_exprs: "list[Callable]") -> Callable:
|
|
205
216
|
def decorated(*args, **kwargs):
|
|
206
|
-
|
|
207
|
-
key(*args, **kwargs)
|
|
217
|
+
items = [
|
|
218
|
+
(key(*args, **kwargs), value(*args, **kwargs))
|
|
208
219
|
for key, value in zip(key_exprs, value_exprs, strict=True)
|
|
209
|
-
|
|
220
|
+
]
|
|
221
|
+
_check_value_size(items, "dict")
|
|
222
|
+
return dict(items)
|
|
210
223
|
|
|
211
224
|
decorated.__name__ = "dict(...)"
|
|
212
225
|
return decorated
|
|
@@ -231,7 +244,7 @@ def build_attribute(value_expr: Callable, attr: str) -> Callable:
|
|
|
231
244
|
def build_expression( # noqa: C901
|
|
232
245
|
node, variable_hook, operator_mapping, allow_value_nodes: bool = False
|
|
233
246
|
):
|
|
234
|
-
"""Build a callable from an AST node using
|
|
247
|
+
"""Build a callable from an AST node using an allowlist of allowed structures.
|
|
235
248
|
|
|
236
249
|
Args:
|
|
237
250
|
allow_value_nodes: when ``True``, value-producing structures (arithmetic,
|
|
@@ -265,7 +278,7 @@ def build_expression( # noqa: C901
|
|
|
265
278
|
expressions.append(expression)
|
|
266
279
|
return reduce(custom_and, expressions)
|
|
267
280
|
case ast.Call(func=ast.Name(id=func_id)):
|
|
268
|
-
# Only
|
|
281
|
+
# Only allowlisted functions from the registry (e.g. ``In(...)``) are
|
|
269
282
|
# callable. Method calls (``obj.method()``) have an ``ast.Attribute``
|
|
270
283
|
# func and fall through to the ``case _`` guard below — this prevents
|
|
271
284
|
# using calls as a sandbox-escape vector.
|
|
@@ -276,10 +289,13 @@ def build_expression( # noqa: C901
|
|
|
276
289
|
return operator_mapping[type(node.op)](recurse(node.operand))
|
|
277
290
|
case ast.UnaryOp(op=(ast.USub() | ast.UAdd())) if allow_value_nodes:
|
|
278
291
|
return build_unaryop(unary_operators[type(node.op)], recurse(node.operand))
|
|
279
|
-
case ast.BinOp() if allow_value_nodes
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
292
|
+
case ast.BinOp() if allow_value_nodes:
|
|
293
|
+
op_type = type(node.op)
|
|
294
|
+
if op_type not in binary_operators:
|
|
295
|
+
# e.g. bitwise ``^``/``|``/``<<`` are outside the allowlist. (``**`` and ``*``
|
|
296
|
+
# are allowed but magnitude-capped, see ``binary_operators``.)
|
|
297
|
+
raise ValueError(f"Binary operator '{op_type.__name__}' is not allowed")
|
|
298
|
+
return build_binop(binary_operators[op_type], recurse(node.left), recurse(node.right))
|
|
283
299
|
case ast.List(elts=elts) if allow_value_nodes:
|
|
284
300
|
return build_collection(list, [recurse(e) for e in elts])
|
|
285
301
|
case ast.Tuple(elts=elts) if allow_value_nodes:
|
|
@@ -300,14 +316,16 @@ def build_expression( # noqa: C901
|
|
|
300
316
|
# without underscore-attribute access or method calls, it cannot reach
|
|
301
317
|
# type objects.
|
|
302
318
|
if attr.startswith("_"):
|
|
303
|
-
raise
|
|
319
|
+
raise UnsupportedExpression(f"Attribute access to '{attr}' is not allowed")
|
|
304
320
|
return build_attribute(recurse(node.value), attr)
|
|
305
321
|
case ast.Name(id=name):
|
|
306
322
|
return variable_hook(name)
|
|
307
323
|
case ast.Constant(value=value):
|
|
308
324
|
return build_constant(value)
|
|
309
325
|
case _:
|
|
310
|
-
raise
|
|
326
|
+
raise UnsupportedExpression(
|
|
327
|
+
f"Unsupported expression structure: {node.__class__.__name__}"
|
|
328
|
+
)
|
|
311
329
|
|
|
312
330
|
|
|
313
331
|
def parse_boolean_expr(expr, variable_hook, operator_mapping):
|
|
@@ -315,16 +333,27 @@ def parse_boolean_expr(expr, variable_hook, operator_mapping):
|
|
|
315
333
|
if expr.strip() == "":
|
|
316
334
|
raise SyntaxError("Empty expression")
|
|
317
335
|
|
|
318
|
-
# Optimization
|
|
319
|
-
|
|
336
|
+
# Optimization: a lone identifier can only be a variable name, so there is
|
|
337
|
+
# nothing to parse. Anything else (operators, comparisons, spaces, calls)
|
|
338
|
+
# goes through the parser.
|
|
339
|
+
if expr.isidentifier():
|
|
320
340
|
return variable_hook(expr)
|
|
321
341
|
expr = replace_operators(expr)
|
|
322
342
|
tree = ast.parse(expr, mode="eval")
|
|
323
343
|
return build_expression(tree.body, variable_hook, operator_mapping)
|
|
324
344
|
|
|
325
345
|
|
|
346
|
+
def names_in_boolean_expr(expr: str) -> "set[str]":
|
|
347
|
+
"""Variable names that :func:`parse_boolean_expr` would resolve in ``expr``."""
|
|
348
|
+
try:
|
|
349
|
+
tree = ast.parse(replace_operators(expr), mode="eval")
|
|
350
|
+
except SyntaxError:
|
|
351
|
+
return set()
|
|
352
|
+
return {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)}
|
|
353
|
+
|
|
354
|
+
|
|
326
355
|
def parse_expr(expr: str, variable_hook: Callable) -> Callable:
|
|
327
|
-
"""Parse a value expression into a callable using the restricted AST
|
|
356
|
+
"""Parse a value expression into a callable using the restricted AST allowlist.
|
|
328
357
|
|
|
329
358
|
Unlike :func:`parse_boolean_expr`, this does not apply the DSL operator
|
|
330
359
|
replacement (``!``/``^``/``v``) and does not coerce the top-level result to
|
|
@@ -333,7 +362,7 @@ def parse_expr(expr: str, variable_hook: Callable) -> Callable:
|
|
|
333
362
|
without :func:`eval`.
|
|
334
363
|
|
|
335
364
|
Raises:
|
|
336
|
-
ValueError: if the expression uses a structure outside the
|
|
365
|
+
ValueError: if the expression uses a structure outside the allowlist
|
|
337
366
|
(e.g. attribute access to dunders, method calls, lambdas).
|
|
338
367
|
SyntaxError: if the expression is empty or not valid Python.
|
|
339
368
|
"""
|
|
@@ -358,14 +387,115 @@ operator_mapping = {
|
|
|
358
387
|
ast.NotEq: build_custom_operator(operator.ne),
|
|
359
388
|
}
|
|
360
389
|
|
|
390
|
+
# Result-size caps for the values an expression can build cheaply. Ordinary arithmetic and
|
|
391
|
+
# collections are well under these; the caps only reject the denial-of-service forms (giant
|
|
392
|
+
# bignums, giant or deeply shared sequences and collections).
|
|
393
|
+
_MAX_POW_RESULT_BITS = 4096
|
|
394
|
+
_MAX_VALUE_SIZE = 1_000_000
|
|
395
|
+
|
|
396
|
+
_TEXT_TYPES = (str, bytes, bytearray)
|
|
397
|
+
_SEQUENCE_TYPES = (*_TEXT_TYPES, list, tuple)
|
|
398
|
+
_CONTAINER_TYPES = (list, tuple, set, frozenset)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _value_size(value) -> int:
|
|
402
|
+
"""Size of ``value`` counting a shared reference every time it appears.
|
|
403
|
+
|
|
404
|
+
``[[0] * 1000] * 1000`` holds a thousand references to one list, so it is cheap to build,
|
|
405
|
+
but comparing, hashing or printing it walks a million elements. That walked size is what
|
|
406
|
+
is measured here, in elements, characters or bytes, memoized by identity so the
|
|
407
|
+
measurement itself stays cheap.
|
|
408
|
+
"""
|
|
409
|
+
memo: "dict[int, int]" = {}
|
|
410
|
+
|
|
411
|
+
def size(item) -> int:
|
|
412
|
+
key = id(item)
|
|
413
|
+
if key in memo:
|
|
414
|
+
return memo[key]
|
|
415
|
+
memo[key] = 1
|
|
416
|
+
if isinstance(item, _TEXT_TYPES):
|
|
417
|
+
total = len(item)
|
|
418
|
+
elif isinstance(item, int):
|
|
419
|
+
total = item.bit_length() // 8
|
|
420
|
+
elif isinstance(item, dict):
|
|
421
|
+
total = sum(size(k) + size(v) for k, v in item.items())
|
|
422
|
+
elif isinstance(item, _CONTAINER_TYPES):
|
|
423
|
+
total = sum(size(element) for element in item)
|
|
424
|
+
else:
|
|
425
|
+
total = 1
|
|
426
|
+
memo[key] = max(total, 1)
|
|
427
|
+
return memo[key]
|
|
428
|
+
|
|
429
|
+
return size(value)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _check_value_size(value, what: str):
|
|
433
|
+
if _value_size(value) > _MAX_VALUE_SIZE:
|
|
434
|
+
raise ValueError(f"{what} is too large for the restricted evaluator")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _guarded_pow(base, exp):
|
|
438
|
+
"""``**`` with a magnitude cap (GHSA-r8gj-366q-cgvj).
|
|
439
|
+
|
|
440
|
+
``int ** int`` can allocate a giant bignum from a tiny expression (``9**9**9`` is a
|
|
441
|
+
~370-million-digit number). The result size is estimated *before* computing, so the
|
|
442
|
+
allocation never happens. Non-integer operands (floats overflow to ``inf`` instead of
|
|
443
|
+
growing without bound) are passed straight through.
|
|
444
|
+
"""
|
|
445
|
+
if isinstance(base, int) and isinstance(exp, int) and exp > 0 and base not in (0, 1, -1):
|
|
446
|
+
if base.bit_length() * exp > _MAX_POW_RESULT_BITS:
|
|
447
|
+
raise ValueError("'**' result is too large for the restricted evaluator")
|
|
448
|
+
return operator.pow(base, exp)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _guarded_mul(a, b):
|
|
452
|
+
"""``*`` with a result-size cap (GHSA-r8gj-366q-cgvj).
|
|
453
|
+
|
|
454
|
+
``seq * n`` (list/str/bytes/tuple times an int) can allocate an enormous object from a
|
|
455
|
+
12-character expression (``[0]*20000000``), and repeatedly squaring an ``int`` doubles its
|
|
456
|
+
size every time. The result size is checked *before* allocating. Other operands (floats)
|
|
457
|
+
are passed straight through.
|
|
458
|
+
"""
|
|
459
|
+
if isinstance(a, int) and isinstance(b, int):
|
|
460
|
+
if a.bit_length() + b.bit_length() > _MAX_POW_RESULT_BITS:
|
|
461
|
+
raise ValueError("'*' result is too large for the restricted evaluator")
|
|
462
|
+
for seq, n in ((a, b), (b, a)):
|
|
463
|
+
if isinstance(seq, _SEQUENCE_TYPES) and isinstance(n, int):
|
|
464
|
+
if n > 0 and n * _value_size(seq) > _MAX_VALUE_SIZE:
|
|
465
|
+
raise ValueError("'*' repetition is too large for the restricted evaluator")
|
|
466
|
+
return operator.mul(a, b)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _guarded_add(a, b):
|
|
470
|
+
"""``+`` with a sequence-length cap: repeated ``x + x`` doubles a sequence every time."""
|
|
471
|
+
sizes = [_value_size(x) for x in (a, b) if isinstance(x, _SEQUENCE_TYPES)]
|
|
472
|
+
if len(sizes) == 2 and sum(sizes) > _MAX_VALUE_SIZE:
|
|
473
|
+
raise ValueError("'+' result is too large for the restricted evaluator")
|
|
474
|
+
return operator.add(a, b)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _guarded_mod(a, b):
|
|
478
|
+
"""``%`` restricted to numbers.
|
|
479
|
+
|
|
480
|
+
printf-style formatting (``'%2000000000d' % 0``) allocates whatever field width the
|
|
481
|
+
expression asks for, so it is rejected on ``str``/``bytes`` operands.
|
|
482
|
+
"""
|
|
483
|
+
if isinstance(a, _TEXT_TYPES):
|
|
484
|
+
raise ValueError("'%' string formatting is not allowed by the restricted evaluator")
|
|
485
|
+
return operator.mod(a, b)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ``**``, ``*``, ``+`` and ``%`` stay available for ordinary arithmetic but are wrapped so a
|
|
489
|
+
# tiny untrusted expression cannot exhaust CPU/memory (GHSA-r8gj-366q-cgvj). ``trusted=True``
|
|
490
|
+
# uses the full Python evaluator instead, without these caps.
|
|
361
491
|
binary_operators = {
|
|
362
|
-
ast.Add:
|
|
492
|
+
ast.Add: _guarded_add,
|
|
363
493
|
ast.Sub: operator.sub,
|
|
364
|
-
ast.Mult:
|
|
494
|
+
ast.Mult: _guarded_mul,
|
|
365
495
|
ast.Div: operator.truediv,
|
|
366
496
|
ast.FloorDiv: operator.floordiv,
|
|
367
|
-
ast.Mod:
|
|
368
|
-
ast.Pow:
|
|
497
|
+
ast.Mod: _guarded_mod,
|
|
498
|
+
ast.Pow: _guarded_pow,
|
|
369
499
|
}
|
|
370
500
|
|
|
371
501
|
unary_operators = {
|
statemachine/state.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from collections.abc import Generator
|
|
2
|
+
from collections.abc import Mapping
|
|
2
3
|
from enum import Enum
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
from typing import Any
|
|
@@ -8,6 +9,7 @@ from weakref import ref
|
|
|
8
9
|
from .callbacks import CallbackGroup
|
|
9
10
|
from .callbacks import CallbackPriority
|
|
10
11
|
from .callbacks import CallbackSpecList
|
|
12
|
+
from .event import Event
|
|
11
13
|
from .event import _expand_event_id
|
|
12
14
|
from .exceptions import InvalidDefinition
|
|
13
15
|
from .i18n import _
|
|
@@ -56,7 +58,7 @@ class _FromState(_TransitionBuilder):
|
|
|
56
58
|
|
|
57
59
|
|
|
58
60
|
class NestedStateFactory(type):
|
|
59
|
-
def __new__( # type: ignore [misc]
|
|
61
|
+
def __new__( # type: ignore [misc] # noqa: C901
|
|
60
62
|
cls, classname, bases, attrs, name="", **kwargs
|
|
61
63
|
) -> "State":
|
|
62
64
|
if not bases:
|
|
@@ -76,6 +78,8 @@ class NestedStateFactory(type):
|
|
|
76
78
|
states = []
|
|
77
79
|
history = []
|
|
78
80
|
callbacks = {}
|
|
81
|
+
# Order is significant: a ``HistoryState`` is a ``State``, and an ``Event`` is a
|
|
82
|
+
# callable ``str``, so both would be captured by a later branch.
|
|
79
83
|
for key, value in attrs.items():
|
|
80
84
|
if isinstance(value, States):
|
|
81
85
|
for state_id, state in value.items():
|
|
@@ -88,7 +92,27 @@ class NestedStateFactory(type):
|
|
|
88
92
|
value._set_id(key)
|
|
89
93
|
states.append(value)
|
|
90
94
|
elif isinstance(value, TransitionList):
|
|
91
|
-
|
|
95
|
+
for event_id in _expand_event_id(key):
|
|
96
|
+
value.add_event(event_id)
|
|
97
|
+
elif isinstance(value, Event):
|
|
98
|
+
event_ids = [value.id] if value._has_real_id else _expand_event_id(key)
|
|
99
|
+
events = [
|
|
100
|
+
Event(
|
|
101
|
+
id=event_id,
|
|
102
|
+
name=value.name,
|
|
103
|
+
delay=value.delay,
|
|
104
|
+
internal=value.internal,
|
|
105
|
+
)
|
|
106
|
+
for event_id in event_ids
|
|
107
|
+
]
|
|
108
|
+
if value._transitions is not None:
|
|
109
|
+
for event in events:
|
|
110
|
+
value._transitions.add_event(event)
|
|
111
|
+
elif getattr(value, "attr_name", None):
|
|
112
|
+
if value.is_event:
|
|
113
|
+
for event_id in _expand_event_id(key):
|
|
114
|
+
value._transitions.add_event(event_id)
|
|
115
|
+
callbacks[value.attr_name] = value
|
|
92
116
|
elif callable(value):
|
|
93
117
|
callbacks[key] = value
|
|
94
118
|
|
|
@@ -296,7 +320,7 @@ class State:
|
|
|
296
320
|
)
|
|
297
321
|
|
|
298
322
|
def __str__(self):
|
|
299
|
-
return self.name
|
|
323
|
+
return str(self.name)
|
|
300
324
|
|
|
301
325
|
@property
|
|
302
326
|
def id(self) -> str:
|
|
@@ -308,7 +332,7 @@ class State:
|
|
|
308
332
|
self.value = id
|
|
309
333
|
if not self.name:
|
|
310
334
|
self.name = humanize_id(self._id)
|
|
311
|
-
self._hash = hash((self.name, self._id))
|
|
335
|
+
self._hash = hash((str(self.name), self._id))
|
|
312
336
|
|
|
313
337
|
return self
|
|
314
338
|
|
|
@@ -366,14 +390,18 @@ class InstanceState(State):
|
|
|
366
390
|
self,
|
|
367
391
|
state: State,
|
|
368
392
|
machine: "StateChart",
|
|
393
|
+
instance_states: "Mapping[Any, State]",
|
|
369
394
|
):
|
|
370
395
|
self._state = state
|
|
371
396
|
self._machine = ref(machine)
|
|
397
|
+
self._instance_states = instance_states
|
|
372
398
|
self._hash = hash(state)
|
|
373
|
-
|
|
399
|
+
# No _init_states(): it would rebind the shared children's ``parent`` to this proxy.
|
|
374
400
|
|
|
375
401
|
def __getattr__(self, name: str):
|
|
376
402
|
value = getattr(self._state, name)
|
|
403
|
+
if isinstance(value, State):
|
|
404
|
+
value = self._instance_states.get(value.value, value)
|
|
377
405
|
self.__dict__[name] = value
|
|
378
406
|
return value
|
|
379
407
|
|
|
@@ -416,13 +444,24 @@ class AnyState(State):
|
|
|
416
444
|
until the state machine class is evaluated.
|
|
417
445
|
"""
|
|
418
446
|
|
|
447
|
+
def __init__(self, *args, **kwargs):
|
|
448
|
+
super().__init__(*args, **kwargs)
|
|
449
|
+
self._copies: dict[tuple[int, int], Transition] = {}
|
|
450
|
+
"""Transition copied onto each state, keyed by ``(id(transition), id(state))``, so an
|
|
451
|
+
attribute that declares several ids adds them all to one copy."""
|
|
452
|
+
|
|
419
453
|
def _on_event_defined(self, event: str, transition: Transition, states: list[State]):
|
|
420
454
|
for state in states:
|
|
421
455
|
if state.final:
|
|
422
456
|
continue
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
457
|
+
key = (id(transition), id(state))
|
|
458
|
+
copy = self._copies.get(key)
|
|
459
|
+
if copy is None:
|
|
460
|
+
copy = transition._copy_with_args(source=state, event=event)
|
|
461
|
+
self._copies[key] = copy
|
|
462
|
+
state.transitions.add_transitions(copy)
|
|
463
|
+
else:
|
|
464
|
+
copy.add_event(event)
|
|
426
465
|
|
|
427
466
|
|
|
428
467
|
class HistoryType(str, Enum):
|
statemachine/statemachine.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
from typing import Generic
|
|
7
7
|
from typing import TypeVar
|
|
8
|
+
from typing import cast
|
|
8
9
|
|
|
9
10
|
from statemachine.orderedset import OrderedSet
|
|
10
11
|
|
|
@@ -146,7 +147,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
146
147
|
listeners: "list[object] | None" = None,
|
|
147
148
|
**kwargs: Any,
|
|
148
149
|
):
|
|
149
|
-
self.model: TModel = model if model is not None else Model()
|
|
150
|
+
self.model: TModel = model if model is not None else cast(TModel, Model())
|
|
150
151
|
"""The external model object that holds domain state, or an internal
|
|
151
152
|
:class:`Model` instance when none is provided. See :ref:`domain models`."""
|
|
152
153
|
|
|
@@ -207,7 +208,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
207
208
|
instance_states: dict[Any, Any] = {}
|
|
208
209
|
events = self.__class__._events
|
|
209
210
|
for state in self.states_map.values():
|
|
210
|
-
ist = InstanceState(state, self)
|
|
211
|
+
ist = InstanceState(state, self, instance_states)
|
|
211
212
|
instance_states[state.value] = ist
|
|
212
213
|
if state.id not in events:
|
|
213
214
|
vars(self)[state.id] = ist
|
|
@@ -225,7 +226,11 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
225
226
|
return run_async_from_sync(result)
|
|
226
227
|
|
|
227
228
|
def _processing_loop(self, caller_future: "Any | None" = None) -> Any:
|
|
228
|
-
result
|
|
229
|
+
# Only the async engine gives an event a future, for its caller to await the result by.
|
|
230
|
+
if isinstance(self._engine, AsyncEngine):
|
|
231
|
+
result = self._engine.processing_loop(caller_future)
|
|
232
|
+
else:
|
|
233
|
+
result = self._engine.processing_loop()
|
|
229
234
|
if not isawaitable(result):
|
|
230
235
|
return result
|
|
231
236
|
return run_async_from_sync(result)
|
|
@@ -259,7 +264,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
259
264
|
|
|
260
265
|
def __setstate__(self, state: dict[str, Any]) -> None:
|
|
261
266
|
listeners = state.pop("_listeners")
|
|
262
|
-
self.
|
|
267
|
+
vars(self).update(state)
|
|
263
268
|
self._callbacks = CallbacksRegistry()
|
|
264
269
|
self._config = self._build_configuration()
|
|
265
270
|
self._listeners = {}
|
|
@@ -273,11 +278,11 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
273
278
|
self._engine.start()
|
|
274
279
|
|
|
275
280
|
def _get_initial_configuration(self):
|
|
276
|
-
initial_state_values =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
initial_state_values = self.start_configuration_values
|
|
282
|
+
if not initial_state_values:
|
|
283
|
+
# Only a machine with no states has no initial state, and none can be created.
|
|
284
|
+
assert self.initial_state is not None
|
|
285
|
+
initial_state_values = [self.initial_state.value]
|
|
281
286
|
try:
|
|
282
287
|
return [self.states_map[value] for value in initial_state_values]
|
|
283
288
|
except KeyError as err:
|
|
@@ -362,7 +367,7 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
362
367
|
return f'<div class="statemachine">{self._repr_svg_()}</div>'
|
|
363
368
|
|
|
364
369
|
def _repr_svg_(self):
|
|
365
|
-
return self
|
|
370
|
+
return format(self, "svg")
|
|
366
371
|
|
|
367
372
|
def _graph(self):
|
|
368
373
|
from .contrib.diagram import DotGraphMachine
|
|
@@ -460,18 +465,23 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
460
465
|
self,
|
|
461
466
|
event: str,
|
|
462
467
|
*args,
|
|
463
|
-
delay: float =
|
|
468
|
+
delay: "float | None" = None,
|
|
464
469
|
send_id: "str | None" = None,
|
|
465
|
-
internal: bool =
|
|
470
|
+
internal: "bool | None" = None,
|
|
466
471
|
**kwargs,
|
|
467
472
|
) -> Any:
|
|
468
473
|
"""Send an :ref:`Event` to the state machine.
|
|
469
474
|
|
|
470
475
|
:param event: The trigger for the state machine, specified as an event id string.
|
|
471
476
|
:param args: Additional positional arguments to pass to the event.
|
|
472
|
-
:param delay: A time delay in milliseconds to process the event.
|
|
477
|
+
:param delay: A time delay in milliseconds to process the event. Defaults to the
|
|
478
|
+
event's declaration, or 0. An explicit ``0`` overrides a declared delay.
|
|
473
479
|
:param send_id: An identifier for the event, used with ``cancel_event()`` to cancel
|
|
474
480
|
delayed events.
|
|
481
|
+
:param internal: Put the event on the internal queue, so it runs within the current
|
|
482
|
+
macrostep. Defaults to the event's declaration. Honored from a callback; sent from
|
|
483
|
+
outside a step, the event goes to the external queue and its result is returned.
|
|
484
|
+
Cannot be combined with ``delay``.
|
|
475
485
|
:param kwargs: Additional keyword arguments to pass to the event.
|
|
476
486
|
|
|
477
487
|
.. seealso::
|
|
@@ -480,9 +490,10 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
480
490
|
"""
|
|
481
491
|
know_event = getattr(self, event, None)
|
|
482
492
|
event_name = know_event.name if know_event else event
|
|
483
|
-
delay
|
|
484
|
-
delay
|
|
485
|
-
|
|
493
|
+
if delay is None:
|
|
494
|
+
delay = cast(float, know_event.delay if know_event else 0)
|
|
495
|
+
if internal is None:
|
|
496
|
+
internal = bool(know_event and know_event.internal)
|
|
486
497
|
event_instance = BoundEvent(
|
|
487
498
|
id=event, name=event_name, delay=delay, internal=internal, _sm=self
|
|
488
499
|
)
|
|
@@ -492,13 +503,22 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
492
503
|
return run_async_from_sync(result)
|
|
493
504
|
|
|
494
505
|
def raise_(
|
|
495
|
-
self,
|
|
506
|
+
self,
|
|
507
|
+
event: str,
|
|
508
|
+
*args,
|
|
509
|
+
delay: "float | None" = None,
|
|
510
|
+
send_id: "str | None" = None,
|
|
511
|
+
**kwargs,
|
|
496
512
|
) -> Any:
|
|
497
513
|
"""Send an :ref:`Event` to the state machine in the internal event queue.
|
|
498
514
|
|
|
499
515
|
Events on the internal queue are processed immediately within the current
|
|
500
516
|
macrostep, before any pending external events. This is equivalent to calling
|
|
501
|
-
``send(..., internal=True)
|
|
517
|
+
``send(..., internal=True)``: called from outside a callback, there is no macrostep
|
|
518
|
+
to join, so the event goes to the external queue and its result is returned. A
|
|
519
|
+
``delay`` other than 0, given or declared on the event, is an
|
|
520
|
+
:class:`~statemachine.exceptions.InvalidDefinition`; pass ``delay=0`` to override a
|
|
521
|
+
declared one.
|
|
502
522
|
|
|
503
523
|
.. seealso::
|
|
504
524
|
|
|
@@ -507,7 +527,10 @@ class StateChart(Generic[TModel], metaclass=StateMachineMetaclass):
|
|
|
507
527
|
return self.send(event, *args, delay=delay, send_id=send_id, internal=True, **kwargs)
|
|
508
528
|
|
|
509
529
|
def cancel_event(self, send_id: str):
|
|
510
|
-
"""Cancel all the delayed events with the given ``send_id``.
|
|
530
|
+
"""Cancel all the delayed events with the given ``send_id``.
|
|
531
|
+
|
|
532
|
+
A caller awaiting one of them gets ``None``.
|
|
533
|
+
"""
|
|
511
534
|
self._engine.cancel_event(send_id)
|
|
512
535
|
|
|
513
536
|
@property
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
statemachine/__init__.py,sha256=QiHL_KGtaPxEuPtKCpwb6DssApC1jcalKYrkfNgMKc4,445
|
|
2
|
-
statemachine/callbacks.py,sha256=LKX9GnDhN64yYh2EvDEYNJg0-N61Kmyd0aqTZKxujM4,14526
|
|
3
|
-
statemachine/configuration.py,sha256=MblvaBwEOyooYc4Vzo-qeryF9sEmx9mdtZDY0Gf_uRU,5991
|
|
4
|
-
statemachine/dispatcher.py,sha256=DyRqwgr7gXom312BIPsNZiueq5n8jNp1dBDExm7mG_g,8109
|
|
5
|
-
statemachine/event.py,sha256=hYVvsNpsY0-OSebwDEpxdLXadoJS_G5pmDe34hRqnFk,7645
|
|
6
|
-
statemachine/event_data.py,sha256=7_hoE0cp-0uW71Kk3erO3eYM6anTT5X3slGMcehIGIo,3111
|
|
7
|
-
statemachine/events.py,sha256=11GyX8GYpsSwvW0v1b4zHUGhzKRoqNjQYtuDj8NvqOM,1212
|
|
8
|
-
statemachine/exceptions.py,sha256=N6iTrin-uMyw31mbXbO6FUj0V5WGvfKaNZe6L2pV0IE,1314
|
|
9
|
-
statemachine/factory.py,sha256=qIgDxBwa5xa0lgvTaV4QPqdYqqkXZCzlSq0sX4TjZvo,14640
|
|
10
|
-
statemachine/graph.py,sha256=ngQ31YAetfQLQvdBU5cHwLQcD-wJL92axPJ2sUeCu1g,1999
|
|
11
|
-
statemachine/i18n.py,sha256=NLvGseaORmQ0G-V_J8tkjoxh_piWMOm2CI6mBQpLamc,362
|
|
12
|
-
statemachine/invoke.py,sha256=5B7XNvBFnQaKByoYM1VbRlJvJyEwXn1C0QKUCaRjmpU,24908
|
|
13
|
-
statemachine/mixins.py,sha256=8qxZZfBwVdFcr3oPFVWHGzgmAubH6VgQus5x3c6VEE0,1706
|
|
14
|
-
statemachine/model.py,sha256=OylI3FjMiHpYyDl9mtK1zEJMeSvemaN4giQDonpc8kI,211
|
|
15
|
-
statemachine/orderedset.py,sha256=7y4Hu6vVD0LHTSJ5iTB9F-faRw0326EubR3AOrpZAHE,2570
|
|
16
|
-
statemachine/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
-
statemachine/registry.py,sha256=TmHfVU7HgaG43slG5lh8EL-8237Fm8r2UkwDj8u-t0c,700
|
|
18
|
-
statemachine/signature.py,sha256=-IiRYJScx4cDKny3ictQvHwQBbaCyxWpRKSgQksMT0E,9834
|
|
19
|
-
statemachine/spec_parser.py,sha256=fjiUjtzTAk6UliNsdbeInqrnp38cfHeYVlgMrbqrzxc,13450
|
|
20
|
-
statemachine/state.py,sha256=kSHeJh1BlSpeDPxMKCCpwIJqzx4MPdXYZ1S24kki4os,15426
|
|
21
|
-
statemachine/statemachine.py,sha256=27TRypjsh3lwlAGZAwqrt-Cnka65uBoaxMWrU27gg6M,19941
|
|
22
|
-
statemachine/states.py,sha256=NRHtATtb-2Ac2nwzSn5OfHwzOSxI2FLeNR8o1v84USs,4827
|
|
23
|
-
statemachine/transition.py,sha256=JtGABHJ6inZrneI70bgu4ir2BZ2YQagcYX9l6TcsHSc,6912
|
|
24
|
-
statemachine/transition_list.py,sha256=zZuGWB86fL2ux-orIE7CdeCcnhaS5p2njIy8h2SuyVY,4361
|
|
25
|
-
statemachine/transition_mixin.py,sha256=i9ZLDGPgE2zIkTn7IAEMjscn1gnI-HJMtxX-YG447tY,3002
|
|
26
|
-
statemachine/utils.py,sha256=taCgrsRDcQQLweyq607AVZyI0NMukRP-HGpO8TiVNgk,1527
|
|
27
|
-
statemachine/contrib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
statemachine/contrib/timeout.py,sha256=VXkdLpxNeUKihkbtLv8XTWdG-meQlSFtxJfcxXgV8Uw,2251
|
|
29
|
-
statemachine/contrib/weighted.py,sha256=lzcopcY0EzjJb-N4APJJc7AlgZIPtthTbBizKaDtOIk,6787
|
|
30
|
-
statemachine/contrib/diagram/__init__.py,sha256=mvTluFDnhE2OzTeOhyU7aQebl79LIOsvOiNFAVIldww,7730
|
|
31
|
-
statemachine/contrib/diagram/__main__.py,sha256=iPpiz09xKqtAjrhONS99OYp6R2dQ6Anbhw1qPIN8ELo,80
|
|
32
|
-
statemachine/contrib/diagram/extract.py,sha256=oNy67KToH7zxgEIPavOvgbpoYTSkM3iWB-Zas1_lcqY,9854
|
|
33
|
-
statemachine/contrib/diagram/formatter.py,sha256=lYdBGO3VSZnM9OipyNsuA6StMEEjrRdnksm9MmzKXBA,4724
|
|
34
|
-
statemachine/contrib/diagram/model.py,sha256=pn7dzrt8U8TOn4A-Jg1Rc3b1UUlTgs75WfR2x-xtEuc,1451
|
|
35
|
-
statemachine/contrib/diagram/sphinx_ext.py,sha256=HcEXU-aYz8da02XPkZKpF5HDF0XPBGGg9FCz8MtI1C8,10338
|
|
36
|
-
statemachine/contrib/diagram/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
37
|
-
statemachine/contrib/diagram/renderers/dot.py,sha256=VkWMVP1DLl1YUEUZVmBVSNQVbAG516v3hQcK4PePWD8,19772
|
|
38
|
-
statemachine/contrib/diagram/renderers/mermaid.py,sha256=3N1z0klonL768exBcWp3WLMuWUgCpRso2UhpdnvHYhY,13424
|
|
39
|
-
statemachine/contrib/diagram/renderers/table.py,sha256=PLVfwTIZHtv7uK3smAfN1owAOEunkBRHyEkh5ygVcfw,3711
|
|
40
|
-
statemachine/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
41
|
-
statemachine/engines/async_.py,sha256=_n7x5IDu6bucBjE7PFdHBzYwrEcPd-iRxDfMc4FE9HI,22537
|
|
42
|
-
statemachine/engines/base.py,sha256=kAgpDu8iTejPBXp_4casK_rQuHkL_G3Njph8iuXzDiw,37954
|
|
43
|
-
statemachine/engines/sync.py,sha256=F_bt4z1KLLcFCP77E9SNWDJMWiMTexE0iOU2XAd_x5A,9153
|
|
44
|
-
statemachine/io/__init__.py,sha256=i5rGVI1ETu1hH0W8qtQMqK0WrFCYLPdV-s5VXh9vOSo,2062
|
|
45
|
-
statemachine/io/actions.py,sha256=FmRjPFJkcQwtxSEaOXbvu3S5vETai2j5r8mC_4eZ8Og,18624
|
|
46
|
-
statemachine/io/builder.py,sha256=yNaQrtFfbs55uRd-loZ-Vs5061liwM-np-B_2ZuJO1I,8350
|
|
47
|
-
statemachine/io/class_factory.py,sha256=H8zxsfrjLw7L_X2g2hoXb2CjpHxTftsoBmPBWIcen-4,9557
|
|
48
|
-
statemachine/io/evaluators.py,sha256=tMcovIr6dhysLRaxUjYbNmPu3jvOzpHOZDjevUnGEMU,10640
|
|
49
|
-
statemachine/io/interpreter.py,sha256=FQUXUlt1PSPzFHyixiLNXcN-7KYBM_i5RAFPRPLu6TE,5567
|
|
50
|
-
statemachine/io/invoke.py,sha256=wbeSJSlDLEvS7hXSznpMT7wd8GDcVsQSXGgFr67zVrI,9877
|
|
51
|
-
statemachine/io/loader.py,sha256=GbCF0MoLxcRa84zZOZPsJpdIUeodFGcsFoWE_Wk4bqQ,6442
|
|
52
|
-
statemachine/io/model.py,sha256=mTJvlbDJndX78pHfyDdEaqc9zTBWVA-OFA5xzVZbz6Y,8695
|
|
53
|
-
statemachine/io/native.py,sha256=HjIOKDQIGExLMglcm8VGbga71Y1Zw-pBbKYWP-y_oiY,13758
|
|
54
|
-
statemachine/io/ports.py,sha256=XoIAQfMujkEnH38t4iu766yiHycb89TNrA3owtBJU2U,2956
|
|
55
|
-
statemachine/io/system_variables.py,sha256=7skuJgBL8giqzaNEyWd783xOZtVRSUHF6ETwt9fVcd4,6223
|
|
56
|
-
statemachine/io/validation.py,sha256=6qiDx0gMkPAehSxJu_mrW4nEF_sebHwa5Dr_njclRcw,1662
|
|
57
|
-
statemachine/io/json/__init__.py,sha256=tFWoOrD9RuKEqkOpI0XifNRP7rguSGY4k-RaSCV_GnM,38
|
|
58
|
-
statemachine/io/json/reader.py,sha256=40V5T-yB1i0Qg-MkK00KHRi7eDQQk4ajfzmKtKXylHs,789
|
|
59
|
-
statemachine/io/schemas/statechart.schema.json,sha256=5yNIS4M7OV6GE2doAWMk_Jct-qDyb5kQ-Dq95Es9KbM,7862
|
|
60
|
-
statemachine/io/scxml/__init__.py,sha256=iGS763p9fZ6A7NVuEVpDVOS303q0dr5N4kVfOZ37jsk,660
|
|
61
|
-
statemachine/io/scxml/processor.py,sha256=Ru_YQAA1OVXNS9AqfN2myL5POq-C4kI1Vpef-eO7k2M,1532
|
|
62
|
-
statemachine/io/scxml/reader.py,sha256=CNUI9ARDJswRZDKqJSBY98Uoa5rQJsIU-kS9Uu5cmzg,17057
|
|
63
|
-
statemachine/io/yaml/__init__.py,sha256=oMapUmquV7hb5qIGMfjDZmDHNq2fqqjJw6LOgGNKeC4,79
|
|
64
|
-
statemachine/io/yaml/reader.py,sha256=mf2FaBttxQuCniSM-vHhSYDGaNLQdzRLoMUHO2kZ0xA,2208
|
|
65
|
-
statemachine/locale/en/LC_MESSAGES/statemachine.po,sha256=_HrrNLQK1krOb56x5Pf1S6LihwaPwhN9_T0PUvumRA0,5338
|
|
66
|
-
statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po,sha256=O15jEhCNATAu6O2WhQ9RhQISB6Ueje2xbbTfEotnVSc,7948
|
|
67
|
-
statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po,sha256=X6-fU4JfK7VPFCRuLQ-4J-YUMF9N_RwhZ1DdCUvn74Y,5814
|
|
68
|
-
statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po,sha256=0_54wD7bl3RBMlYyokNFUdgMCx_1MbKzclxBUwmCNEs,5363
|
|
69
|
-
python_statemachine-3.2.0.dist-info/METADATA,sha256=Vdy46mBCvmA04ruIBaTS9XklmK72ip3yycmrzgCWu48,12642
|
|
70
|
-
python_statemachine-3.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
71
|
-
python_statemachine-3.2.0.dist-info/licenses/LICENSE,sha256=zcP7TsJMqaFxuTvLRZPT7nJl3_ppjxR9Z76BE9pL5zc,1074
|
|
72
|
-
python_statemachine-3.2.0.dist-info/RECORD,,
|
{python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|