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.
Files changed (60) hide show
  1. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.1.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
  4. statemachine/__init__.py +1 -1
  5. statemachine/callbacks.py +5 -11
  6. statemachine/configuration.py +5 -6
  7. statemachine/contrib/diagram/extract.py +26 -27
  8. statemachine/contrib/diagram/formatter.py +5 -7
  9. statemachine/contrib/diagram/model.py +9 -11
  10. statemachine/contrib/diagram/renderers/dot.py +20 -26
  11. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  12. statemachine/contrib/diagram/renderers/table.py +7 -9
  13. statemachine/contrib/weighted.py +7 -11
  14. statemachine/dispatcher.py +18 -13
  15. statemachine/engines/async_.py +20 -9
  16. statemachine/engines/base.py +26 -17
  17. statemachine/event.py +1 -2
  18. statemachine/exceptions.py +2 -2
  19. statemachine/factory.py +11 -15
  20. statemachine/graph.py +2 -2
  21. statemachine/invoke.py +12 -11
  22. statemachine/io/__init__.py +52 -225
  23. statemachine/io/actions.py +627 -0
  24. statemachine/io/builder.py +195 -0
  25. statemachine/io/class_factory.py +236 -0
  26. statemachine/io/evaluators.py +303 -0
  27. statemachine/io/interpreter.py +128 -0
  28. statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
  29. statemachine/io/json/__init__.py +1 -0
  30. statemachine/io/json/reader.py +27 -0
  31. statemachine/io/loader.py +175 -0
  32. statemachine/io/model.py +268 -0
  33. statemachine/io/native.py +402 -0
  34. statemachine/io/ports.py +83 -0
  35. statemachine/io/schemas/statechart.schema.json +258 -0
  36. statemachine/io/scxml/__init__.py +15 -0
  37. statemachine/io/scxml/processor.py +23 -253
  38. statemachine/io/scxml/{parser.py → reader.py} +88 -56
  39. statemachine/io/system_variables.py +199 -0
  40. statemachine/io/validation.py +44 -0
  41. statemachine/io/yaml/__init__.py +1 -0
  42. statemachine/io/yaml/reader.py +65 -0
  43. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  44. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/orderedset.py +15 -6
  48. statemachine/registry.py +1 -4
  49. statemachine/signature.py +2 -5
  50. statemachine/spec_parser.py +228 -45
  51. statemachine/state.py +7 -8
  52. statemachine/statemachine.py +18 -20
  53. statemachine/states.py +3 -5
  54. statemachine/transition.py +3 -4
  55. statemachine/transition_list.py +4 -5
  56. statemachine/transition_mixin.py +1 -1
  57. python_statemachine-3.1.2.dist-info/RECORD +0 -58
  58. statemachine/io/scxml/actions.py +0 -650
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
@@ -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: Dict[str, Callable] = {}
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 ValueError(f"Unsupported function: {func_id}")
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 build_expression(node, variable_hook, operator_mapping): # noqa: C901
182
- if isinstance(node, ast.BoolOp):
183
- # Handle `and` / `or` operations
184
- operator_fn = operator_mapping[type(node.op)]
185
- left_expr = build_expression(node.values[0], variable_hook, operator_mapping)
186
- for right in node.values[1:]:
187
- right_expr = build_expression(right, variable_hook, operator_mapping)
188
- left_expr = operator_fn(left_expr, right_expr)
189
- return left_expr
190
- elif isinstance(node, ast.Compare):
191
- # Handle `==` / `!=` / `>` / `<` / `>=` / `<=` operations
192
- expressions = []
193
- left_expr = build_expression(node.left, variable_hook, operator_mapping)
194
- for right_op, right in zip(node.ops, node.comparators): # noqa: B905 # strict=True requires 3.10+
195
- right_expr = build_expression(right, variable_hook, operator_mapping)
196
- operator_fn = operator_mapping[type(right_op)]
197
- expression = operator_fn(left_expr, right_expr)
198
- left_expr = right_expr
199
- expressions.append(expression)
200
-
201
- return reduce(custom_and, expressions)
202
- elif isinstance(node, ast.Call):
203
- # Handle function calls
204
- assert isinstance(node.func, ast.Name)
205
- constructor = Functions.get(node.func.id)
206
- params = [arg.value for arg in node.args if isinstance(arg, ast.Constant)]
207
- return constructor(*params)
208
- elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
209
- # Handle `not` operation
210
- operand_expr = build_expression(node.operand, variable_hook, operator_mapping)
211
- return operator_mapping[type(node.op)](operand_expr)
212
- elif isinstance(node, ast.Name):
213
- # Handle variables by calling the variable_hook
214
- return variable_hook(node.id)
215
- elif isinstance(node, ast.Constant):
216
- # Handle constants by returning the value
217
- return build_constant(node.value)
218
- else:
219
- raise ValueError(f"Unsupported expression structure: {node.__class__.__name__}")
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 trying to avoid parsing the expression if not needed
228
- if "!" not in expr and " " not in expr and "In(" not in expr:
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: "List[State] | None" = None,
218
- history: "List[HistoryState] | None" = None,
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: List["State"]):
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: List[State]):
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
@@ -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: List[Any] = []
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: Dict[Any, "State"]
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: "List[State]"
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: "Dict[Event, None]"
135
+ _events: "dict[Event, None]"
138
136
  _protected_attrs: set
139
137
  _specs: CallbackSpecList
140
- _class_listeners: List[Any]
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: "List[object] | None" = None,
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: Dict[str, List[State]] = {}
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: Dict[int, Any] = {}
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) -> List[object]:
188
- resolved: List[object] = []
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: Dict[str, Any] = {}
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.id] = ist
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: Dict[str, Any]) -> None:
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: List[object]):
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) -> List[object]:
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) -> "List[Event]":
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) -> "List[Event]":
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 = Type[Enum]
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: "Dict[str, State] | None" = None) -> None:
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: Dict[str, State] = states if states is not None else {}
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)}"
@@ -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 | List[State] | None" = None,
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: "List[State]" = target
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) -> "List[State]":
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
 
@@ -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: List[Transition] = list(transitions) if transitions else []
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: List["State"]):
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) -> List["Event"]:
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.
@@ -1,5 +1,5 @@
1
+ from collections.abc import Callable
1
2
  from typing import Any
2
- from typing import Callable
3
3
  from typing import TypeVar
4
4
 
5
5
  from .callbacks import CallbackGroup