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.
Files changed (40) hide show
  1. {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/METADATA +7 -6
  2. python_statemachine-3.2.2.dist-info/RECORD +73 -0
  3. {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +11 -0
  6. statemachine/contrib/diagram/__init__.py +15 -6
  7. statemachine/contrib/diagram/extract.py +3 -3
  8. statemachine/contrib/diagram/formatter.py +4 -2
  9. statemachine/contrib/diagram/graphviz.py +66 -0
  10. statemachine/contrib/diagram/sphinx_ext.py +28 -7
  11. statemachine/dispatcher.py +8 -3
  12. statemachine/engines/async_.py +224 -180
  13. statemachine/engines/base.py +257 -245
  14. statemachine/engines/sync.py +224 -108
  15. statemachine/event.py +29 -10
  16. statemachine/event_data.py +19 -3
  17. statemachine/events.py +5 -2
  18. statemachine/exceptions.py +1 -1
  19. statemachine/factory.py +126 -29
  20. statemachine/io/__init__.py +18 -11
  21. statemachine/io/actions.py +124 -15
  22. statemachine/io/evaluators.py +37 -9
  23. statemachine/io/invoke.py +12 -0
  24. statemachine/io/loader.py +17 -3
  25. statemachine/io/schemas/statechart.schema.json +8 -1
  26. statemachine/io/scxml/__init__.py +6 -3
  27. statemachine/io/scxml/processor.py +1 -1
  28. statemachine/io/scxml/reader.py +24 -9
  29. statemachine/io/system_variables.py +34 -19
  30. statemachine/locale/en/LC_MESSAGES/statemachine.po +58 -17
  31. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +66 -17
  32. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +66 -17
  33. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +60 -17
  34. statemachine/orderedset.py +12 -3
  35. statemachine/registry.py +6 -1
  36. statemachine/spec_parser.py +151 -21
  37. statemachine/state.py +47 -8
  38. statemachine/statemachine.py +42 -19
  39. python_statemachine-3.2.0.dist-info/RECORD +0 -72
  40. {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -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 ValueError(f"Unsupported function: {func_id}")
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
- return factory(item(*args, **kwargs) for item in item_exprs)
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
- return {
207
- key(*args, **kwargs): value(*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 a whitelist of allowed structures.
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 whitelisted functions from the registry (e.g. ``In(...)``) are
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 and type(node.op) in binary_operators:
280
- return build_binop(
281
- binary_operators[type(node.op)], recurse(node.left), recurse(node.right)
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 ValueError(f"Attribute access to '{attr}' is not allowed")
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 ValueError(f"Unsupported expression structure: {node.__class__.__name__}")
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 trying to avoid parsing the expression if not needed
319
- if "!" not in expr and " " not in expr and "In(" not in expr:
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 whitelist.
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 whitelist
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: operator.add,
492
+ ast.Add: _guarded_add,
363
493
  ast.Sub: operator.sub,
364
- ast.Mult: operator.mul,
494
+ ast.Mult: _guarded_mul,
365
495
  ast.Div: operator.truediv,
366
496
  ast.FloorDiv: operator.floordiv,
367
- ast.Mod: operator.mod,
368
- ast.Pow: operator.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
- value.add_event(_expand_event_id(key))
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
- self._init_states()
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
- new_transition = transition._copy_with_args(source=state, event=event)
424
-
425
- state.transitions.add_transitions(new_transition)
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):
@@ -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() # type: ignore[assignment]
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 = self._engine.processing_loop(caller_future)
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.__dict__.update(state) # type: ignore[attr-defined]
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
- self.start_configuration_values
278
- if self.start_configuration_values
279
- else [self.initial_state.value] # type: ignore[union-attr]
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._graph().create_svg().decode() # type: ignore[attr-defined]
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 = 0,
468
+ delay: "float | None" = None,
464
469
  send_id: "str | None" = None,
465
- internal: bool = False,
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. Default is 0.
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 if delay else know_event and know_event.delay or 0
485
- ) # first the param, then the event, or 0
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, event: str, *args, delay: float = 0, send_id: "str | None" = None, **kwargs
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,,