python-statemachine 3.1.1__py3-none-any.whl → 3.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/METADATA +16 -3
  2. python_statemachine-3.2.0.dist-info/RECORD +72 -0
  3. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.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/__init__.py +15 -6
  8. statemachine/contrib/diagram/extract.py +23 -24
  9. statemachine/contrib/diagram/formatter.py +5 -7
  10. statemachine/contrib/diagram/model.py +9 -11
  11. statemachine/contrib/diagram/renderers/dot.py +20 -26
  12. statemachine/contrib/diagram/renderers/mermaid.py +36 -40
  13. statemachine/contrib/diagram/renderers/table.py +7 -9
  14. statemachine/contrib/weighted.py +7 -11
  15. statemachine/dispatcher.py +13 -12
  16. statemachine/engines/async_.py +5 -6
  17. statemachine/engines/base.py +12 -14
  18. statemachine/event.py +1 -2
  19. statemachine/exceptions.py +1 -1
  20. statemachine/factory.py +12 -16
  21. statemachine/graph.py +2 -2
  22. statemachine/invoke.py +12 -11
  23. statemachine/io/__init__.py +45 -225
  24. statemachine/io/{scxml/actions.py → actions.py} +158 -288
  25. statemachine/io/builder.py +195 -0
  26. statemachine/io/class_factory.py +236 -0
  27. statemachine/io/evaluators.py +275 -0
  28. statemachine/io/interpreter.py +128 -0
  29. statemachine/io/{scxml/invoke.py → invoke.py} +77 -49
  30. statemachine/io/json/__init__.py +1 -0
  31. statemachine/io/json/reader.py +27 -0
  32. statemachine/io/loader.py +161 -0
  33. statemachine/io/model.py +268 -0
  34. statemachine/io/native.py +402 -0
  35. statemachine/io/ports.py +83 -0
  36. statemachine/io/schemas/statechart.schema.json +258 -0
  37. statemachine/io/scxml/__init__.py +12 -0
  38. statemachine/io/scxml/processor.py +23 -253
  39. statemachine/io/scxml/{parser.py → reader.py} +64 -47
  40. statemachine/io/system_variables.py +184 -0
  41. statemachine/io/validation.py +44 -0
  42. statemachine/io/yaml/__init__.py +1 -0
  43. statemachine/io/yaml/reader.py +65 -0
  44. statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
  45. statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
  46. statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
  47. statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +20 -22
  48. statemachine/orderedset.py +3 -3
  49. statemachine/registry.py +1 -4
  50. statemachine/signature.py +2 -5
  51. statemachine/spec_parser.py +171 -42
  52. statemachine/state.py +5 -6
  53. statemachine/statemachine.py +18 -20
  54. statemachine/states.py +3 -5
  55. statemachine/transition.py +3 -4
  56. statemachine/transition_list.py +4 -5
  57. statemachine/transition_mixin.py +1 -1
  58. python_statemachine-3.1.1.dist-info/RECORD +0 -58
  59. statemachine/io/scxml/schema.py +0 -175
  60. {python_statemachine-3.1.1.dist-info → python_statemachine-3.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Dict
3
- from typing import List
4
- from typing import Optional
5
- from typing import Set
6
2
 
7
3
  from ..model import ActionType
8
4
  from ..model import DiagramAction
@@ -34,14 +30,14 @@ class MermaidRenderer:
34
30
  Compound states outside parallel regions are left unchanged.
35
31
  """
36
32
 
37
- def __init__(self, config: Optional[MermaidRendererConfig] = None):
33
+ def __init__(self, config: MermaidRendererConfig | None = None):
38
34
  self.config = config or MermaidRendererConfig()
39
- self._active_ids: List[str] = []
40
- self._rendered_transitions: Set[tuple] = set()
41
- self._compound_ids: Set[str] = set()
42
- self._initial_child_map: Dict[str, str] = {}
43
- self._parallel_descendant_ids: Set[str] = set()
44
- self._all_descendants_map: Dict[str, Set[str]] = {}
35
+ self._active_ids: list[str] = []
36
+ self._rendered_transitions: set[tuple] = set()
37
+ self._compound_ids: set[str] = set()
38
+ self._initial_child_map: dict[str, str] = {}
39
+ self._parallel_descendant_ids: set[str] = set()
40
+ self._all_descendants_map: dict[str, set[str]] = {}
45
41
 
46
42
  def render(self, graph: DiagramGraph) -> str:
47
43
  """Render a DiagramGraph to a Mermaid stateDiagram-v2 string."""
@@ -52,7 +48,7 @@ class MermaidRenderer:
52
48
  self._parallel_descendant_ids = self._collect_parallel_descendants(graph.states)
53
49
  self._all_descendants_map = self._build_all_descendants_map(graph.states)
54
50
 
55
- lines: List[str] = []
51
+ lines: list[str] = []
56
52
  lines.append("stateDiagram-v2")
57
53
  lines.append(f" direction {self.config.direction}")
58
54
 
@@ -70,9 +66,9 @@ class MermaidRenderer:
70
66
 
71
67
  return "\n".join(lines) + "\n"
72
68
 
73
- def _build_initial_child_map(self, states: List[DiagramState]) -> Dict[str, str]:
69
+ def _build_initial_child_map(self, states: list[DiagramState]) -> dict[str, str]:
74
70
  """Build a map from compound state ID to its initial child ID (recursive)."""
75
- result: Dict[str, str] = {}
71
+ result: dict[str, str] = {}
76
72
  for state in states:
77
73
  if state.children:
78
74
  initial = next((c for c in state.children if c.is_initial), None)
@@ -83,11 +79,11 @@ class MermaidRenderer:
83
79
 
84
80
  @staticmethod
85
81
  def _collect_parallel_descendants(
86
- states: List[DiagramState],
82
+ states: list[DiagramState],
87
83
  inside_parallel: bool = False,
88
- ) -> Set[str]:
84
+ ) -> set[str]:
89
85
  """Collect IDs of all states that are descendants of a parallel state."""
90
- result: Set[str] = set()
86
+ result: set[str] = set()
91
87
  for state in states:
92
88
  if inside_parallel:
93
89
  result.add(state.id)
@@ -97,9 +93,9 @@ class MermaidRenderer:
97
93
  )
98
94
  return result
99
95
 
100
- def _build_all_descendants_map(self, states: List[DiagramState]) -> Dict[str, Set[str]]:
96
+ def _build_all_descendants_map(self, states: list[DiagramState]) -> dict[str, set[str]]:
101
97
  """Map each compound state ID to the set of all its descendant IDs."""
102
- result: Dict[str, Set[str]] = {}
98
+ result: dict[str, set[str]] = {}
103
99
  for state in states:
104
100
  if state.children:
105
101
  result[state.id] = self._collect_recursive_descendants(state.children)
@@ -107,9 +103,9 @@ class MermaidRenderer:
107
103
  return result
108
104
 
109
105
  @staticmethod
110
- def _collect_recursive_descendants(states: List[DiagramState]) -> Set[str]:
106
+ def _collect_recursive_descendants(states: list[DiagramState]) -> set[str]:
111
107
  """Collect all state IDs in a subtree recursively."""
112
- ids: Set[str] = set()
108
+ ids: set[str] = set()
113
109
  for s in states:
114
110
  ids.add(s.id)
115
111
  ids.update(MermaidRenderer._collect_recursive_descendants(s.children))
@@ -132,9 +128,9 @@ class MermaidRenderer:
132
128
 
133
129
  def _render_states(
134
130
  self,
135
- states: List[DiagramState],
136
- transitions: List[DiagramTransition],
137
- lines: List[str],
131
+ states: list[DiagramState],
132
+ transitions: list[DiagramTransition],
133
+ lines: list[str],
138
134
  indent: int,
139
135
  ) -> None:
140
136
  for state in states:
@@ -167,7 +163,7 @@ class MermaidRenderer:
167
163
  def _render_atomic_state(
168
164
  self,
169
165
  state: DiagramState,
170
- lines: List[str],
166
+ lines: list[str],
171
167
  indent: int,
172
168
  ) -> None:
173
169
  pad = " " * indent
@@ -186,8 +182,8 @@ class MermaidRenderer:
186
182
  def _render_compound_state(
187
183
  self,
188
184
  state: DiagramState,
189
- transitions: List[DiagramTransition],
190
- lines: List[str],
185
+ transitions: list[DiagramTransition],
186
+ lines: list[str],
191
187
  indent: int,
192
188
  ) -> None:
193
189
  pad = " " * indent
@@ -227,18 +223,18 @@ class MermaidRenderer:
227
223
  if state.is_active:
228
224
  self._active_ids.append(state.id)
229
225
 
230
- def _collect_all_descendant_ids(self, states: List[DiagramState]) -> Set[str]:
226
+ def _collect_all_descendant_ids(self, states: list[DiagramState]) -> set[str]:
231
227
  """Collect all state IDs in a subtree (direct children only for scope)."""
232
- ids: Set[str] = set()
228
+ ids: set[str] = set()
233
229
  for s in states:
234
230
  ids.add(s.id)
235
231
  return ids
236
232
 
237
233
  def _render_scope_transitions(
238
234
  self,
239
- transitions: List[DiagramTransition],
240
- scope_ids: Set[str],
241
- lines: List[str],
235
+ transitions: list[DiagramTransition],
236
+ scope_ids: set[str],
237
+ lines: list[str],
242
238
  indent: int,
243
239
  ) -> None:
244
240
  """Render transitions that belong to this scope level.
@@ -257,8 +253,8 @@ class MermaidRenderer:
257
253
  are redirected to the compound's initial child via ``_resolve_endpoint``.
258
254
  """
259
255
  # Build the descendant sets for compounds in this scope
260
- compound_descendants: Dict[str, Set[str]] = {}
261
- expanded: Set[str] = set(scope_ids)
256
+ compound_descendants: dict[str, set[str]] = {}
257
+ expanded: set[str] = set(scope_ids)
262
258
  for sid in scope_ids:
263
259
  if sid in self._all_descendants_map:
264
260
  compound_descendants[sid] = self._all_descendants_map[sid]
@@ -295,8 +291,8 @@ class MermaidRenderer:
295
291
  @staticmethod
296
292
  def _is_fully_internal(
297
293
  source: str,
298
- targets: List[str],
299
- compound_descendants: Dict[str, Set[str]],
294
+ targets: list[str],
295
+ compound_descendants: dict[str, set[str]],
300
296
  ) -> bool:
301
297
  """Check if all endpoints belong to the same compound's descendants."""
302
298
  for descendants in compound_descendants.values():
@@ -309,11 +305,11 @@ class MermaidRenderer:
309
305
  transition: DiagramTransition,
310
306
  source: str,
311
307
  target: str,
312
- lines: List[str],
308
+ lines: list[str],
313
309
  indent: int,
314
310
  ) -> None:
315
311
  pad = " " * indent
316
- label_parts: List[str] = []
312
+ label_parts: list[str] = []
317
313
  if transition.event:
318
314
  label_parts.append(transition.event)
319
315
  if transition.guards:
@@ -333,8 +329,8 @@ class MermaidRenderer:
333
329
 
334
330
  def _render_initial_and_final(
335
331
  self,
336
- states: List[DiagramState],
337
- lines: List[str],
332
+ states: list[DiagramState],
333
+ lines: list[str],
338
334
  indent: int,
339
335
  ) -> None:
340
336
  """Render top-level [*] --> initial and final --> [*] arrows."""
@@ -1,5 +1,3 @@
1
- from typing import List
2
-
3
1
  from ..model import DiagramGraph
4
2
  from ..model import DiagramState
5
3
  from ..model import DiagramTransition
@@ -26,11 +24,11 @@ class TransitionTableRenderer:
26
24
 
27
25
  def _collect_rows(
28
26
  self,
29
- states: List[DiagramState],
30
- transitions: List[DiagramTransition],
31
- ) -> "List[tuple[str, str, str, str]]":
27
+ states: list[DiagramState],
28
+ transitions: list[DiagramTransition],
29
+ ) -> "list[tuple[str, str, str, str]]":
32
30
  """Collect (State, Event, Guard, Target) tuples from the IR."""
33
- rows: List[tuple[str, str, str, str]] = []
31
+ rows: list[tuple[str, str, str, str]] = []
34
32
  state_names = self._build_state_name_map(states)
35
33
 
36
34
  for t in transitions:
@@ -50,7 +48,7 @@ class TransitionTableRenderer:
50
48
 
51
49
  return rows
52
50
 
53
- def _build_state_name_map(self, states: List[DiagramState]) -> dict:
51
+ def _build_state_name_map(self, states: list[DiagramState]) -> dict:
54
52
  """Build a mapping from state ID to display name, recursively."""
55
53
  result: dict = {}
56
54
  for state in states:
@@ -59,7 +57,7 @@ class TransitionTableRenderer:
59
57
  result.update(self._build_state_name_map(state.children))
60
58
  return result
61
59
 
62
- def _render_md(self, rows: "List[tuple[str, str, str, str]]") -> str:
60
+ def _render_md(self, rows: "list[tuple[str, str, str, str]]") -> str:
63
61
  """Render as a markdown table."""
64
62
  headers = ("State", "Event", "Guard", "Target")
65
63
  col_widths = [len(h) for h in headers]
@@ -79,7 +77,7 @@ class TransitionTableRenderer:
79
77
 
80
78
  return "\n".join(lines) + "\n"
81
79
 
82
- def _render_rst(self, rows: "List[tuple[str, str, str, str]]") -> str:
80
+ def _render_rst(self, rows: "list[tuple[str, str, str, str]]") -> str:
83
81
  """Render as an RST grid table."""
84
82
  headers = ("State", "Event", "Guard", "Target")
85
83
  col_widths = [len(h) for h in headers]
@@ -1,10 +1,7 @@
1
1
  import random
2
2
  from typing import TYPE_CHECKING
3
3
  from typing import Any
4
- from typing import Dict
5
- from typing import List
6
- from typing import Tuple
7
- from typing import Union
4
+ from typing import TypeAlias
8
5
 
9
6
  from statemachine.callbacks import CallbackPriority
10
7
  from statemachine.transition_list import TransitionList
@@ -20,7 +17,7 @@ class _WeightedGroup:
20
17
  the selected index. Subsequent conds check against the cache.
21
18
  """
22
19
 
23
- def __init__(self, weights: List[float], seed: "int | float | str | None" = None):
20
+ def __init__(self, weights: list[float], seed: "int | float | str | None" = None):
24
21
  self.weights = weights
25
22
  self.rng = random.Random(seed)
26
23
  self._selected: "int | None" = None
@@ -60,10 +57,9 @@ def _make_weighted_cond(index: int, group: _WeightedGroup, weight: float, total_
60
57
 
61
58
  # Type alias for a weighted destination:
62
59
  # (target, weight) or (target, weight, kwargs_dict)
63
- _WeightedDest = Union[
64
- Tuple["State", Union[int, float]],
65
- Tuple["State", Union[int, float], Dict[str, Any]],
66
- ]
60
+ _WeightedDest: TypeAlias = (
61
+ tuple["State", int | float] | tuple["State", int | float, dict[str, Any]]
62
+ )
67
63
 
68
64
 
69
65
  def to(target: "State", weight: "int | float", **kwargs: Any) -> _WeightedDest:
@@ -97,7 +93,7 @@ def to(target: "State", weight: "int | float", **kwargs: Any) -> _WeightedDest:
97
93
  return (target, weight, kwargs)
98
94
 
99
95
 
100
- def _validate_dest(i: int, item: Any) -> "Tuple[State, float, Dict[str, Any]]":
96
+ def _validate_dest(i: int, item: Any) -> "tuple[State, float, dict[str, Any]]":
101
97
  """Validate and normalize a single ``(target, weight[, kwargs])`` tuple."""
102
98
  from statemachine.state import State
103
99
 
@@ -109,7 +105,7 @@ def _validate_dest(i: int, item: Any) -> "Tuple[State, float, Dict[str, Any]]":
109
105
 
110
106
  if len(item) == 2:
111
107
  target, weight = item
112
- kwargs: Dict[str, Any] = {}
108
+ kwargs: dict[str, Any] = {}
113
109
  else:
114
110
  target, weight, kwargs = item
115
111
  if not isinstance(kwargs, dict):
@@ -1,14 +1,11 @@
1
+ from collections.abc import Callable
2
+ from collections.abc import Iterable
1
3
  from dataclasses import dataclass
2
4
  from functools import partial
3
5
  from functools import reduce
4
6
  from operator import attrgetter
5
7
  from typing import TYPE_CHECKING
6
8
  from typing import Any
7
- from typing import Callable
8
- from typing import Iterable
9
- from typing import List
10
- from typing import Set
11
- from typing import Tuple
12
9
 
13
10
  from .callbacks import SPECS_ALL
14
11
  from .callbacks import SpecReference
@@ -37,7 +34,7 @@ class Listener:
37
34
  """
38
35
 
39
36
  obj: object
40
- all_attrs: Set[str]
37
+ all_attrs: set[str]
41
38
  resolver_id: str
42
39
 
43
40
  @classmethod
@@ -58,8 +55,8 @@ class Listener:
58
55
  class Listeners:
59
56
  """Listeners that provides attributes to be used as callbacks."""
60
57
 
61
- items: Tuple[Listener, ...]
62
- all_attrs: Set[str]
58
+ items: tuple[Listener, ...]
59
+ all_attrs: set[str]
63
60
 
64
61
  @classmethod
65
62
  def from_listeners(cls, listeners: Iterable["Listener"]) -> "Listeners":
@@ -86,7 +83,7 @@ class Listeners:
86
83
  executor.add(key, spec, builder)
87
84
 
88
85
  def _take_callback(self, name: str, names_not_found_handler: Callable) -> Callable:
89
- callbacks: List[Callable] = []
86
+ callbacks: list[Callable] = []
90
87
  for key, builder in self.search_name(name):
91
88
  callback = builder()
92
89
  callback.unique_key = key # type: ignore[attr-defined]
@@ -114,7 +111,7 @@ class Listeners:
114
111
 
115
112
  # Resolves boolean expressions
116
113
 
117
- names_not_found: Set[str] = set()
114
+ names_not_found: set[str] = set()
118
115
  take_callback_partial = partial(
119
116
  self._take_callback, names_not_found_handler=names_not_found.add
120
117
  )
@@ -162,7 +159,11 @@ class Listeners:
162
159
  if not spec.is_bounded:
163
160
  for listener in self.items:
164
161
  func = getattr(listener.obj, spec.attr_name, None)
165
- if func is not None and func.__func__ is spec.func:
162
+ # ``getattr`` may return a non-method that happens to share the name
163
+ # (e.g. a model attribute named like a compiled guard); it is not the
164
+ # unbounded method we are rebinding, so skip it instead of accessing
165
+ # ``__func__`` (which would raise on a plain value).
166
+ if getattr(func, "__func__", None) is spec.func:
166
167
  yield listener.build_key(spec.attr_name), partial(callable_method, func)
167
168
  return
168
169
 
@@ -228,5 +229,5 @@ def event_method(func) -> Callable:
228
229
  return method
229
230
 
230
231
 
231
- def resolver_factory_from_objects(*objects: Tuple[Any, ...]):
232
+ def resolver_factory_from_objects(*objects: tuple[Any, ...]):
232
233
  return Listeners.from_listeners(Listener.from_obj(o) for o in objects)
@@ -1,10 +1,9 @@
1
1
  import asyncio
2
2
  import contextvars
3
+ from collections.abc import Callable
3
4
  from itertools import chain
4
5
  from time import time
5
6
  from typing import TYPE_CHECKING
6
- from typing import Callable
7
- from typing import List
8
7
 
9
8
  from ..event_data import EventData
10
9
  from ..event_data import TriggerData
@@ -143,7 +142,7 @@ class AsyncEngine(BaseEngine):
143
142
 
144
143
  async def _execute_transition_content(
145
144
  self,
146
- enabled_transitions: "List[Transition]",
145
+ enabled_transitions: "list[Transition]",
147
146
  trigger_data: TriggerData,
148
147
  get_key: "Callable[[Transition], str]",
149
148
  set_target_as_state: bool = False,
@@ -164,7 +163,7 @@ class AsyncEngine(BaseEngine):
164
163
  return result
165
164
 
166
165
  async def _exit_states( # type: ignore[override]
167
- self, enabled_transitions: "List[Transition]", trigger_data: TriggerData
166
+ self, enabled_transitions: "list[Transition]", trigger_data: TriggerData
168
167
  ) -> "OrderedSet[State]":
169
168
  ordered_states, result = self._prepare_exit_states(enabled_transitions)
170
169
  on_error = self._on_error_handler()
@@ -188,7 +187,7 @@ class AsyncEngine(BaseEngine):
188
187
 
189
188
  async def _enter_states( # noqa: C901
190
189
  self,
191
- enabled_transitions: "List[Transition]",
190
+ enabled_transitions: "list[Transition]",
192
191
  trigger_data: TriggerData,
193
192
  states_to_exit: "OrderedSet[State]",
194
193
  previous_configuration: "OrderedSet[State]",
@@ -270,7 +269,7 @@ class AsyncEngine(BaseEngine):
270
269
 
271
270
  return result
272
271
 
273
- async def microstep(self, transitions: "List[Transition]", trigger_data: TriggerData):
272
+ async def microstep(self, transitions: "list[Transition]", trigger_data: TriggerData):
274
273
  self._microstep_count += 1
275
274
  self._debug(
276
275
  "%s macro:%d micro:%d transitions: %s",
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from collections.abc import Callable
2
3
  from dataclasses import dataclass
3
4
  from dataclasses import field
4
5
  from itertools import chain
@@ -7,9 +8,6 @@ from queue import Queue
7
8
  from threading import Lock
8
9
  from typing import TYPE_CHECKING
9
10
  from typing import Any
10
- from typing import Callable
11
- from typing import Dict
12
- from typing import List
13
11
  from typing import cast
14
12
 
15
13
  from ..event import BoundEvent
@@ -91,7 +89,7 @@ class BaseEngine:
91
89
  self._sentinel = object()
92
90
  self.running = True
93
91
  self._processing = Lock()
94
- self._cache: Dict = {} # Cache for _get_args_kwargs results
92
+ self._cache: dict = {} # Cache for _get_args_kwargs results
95
93
  self._invoke_manager = InvokeManager(self)
96
94
  self._macrostep_count: int = 0
97
95
  self._microstep_count: int = 0
@@ -243,7 +241,7 @@ class BaseEngine:
243
241
 
244
242
  return filtered_transitions
245
243
 
246
- def _compute_exit_set(self, transitions: List[Transition]) -> OrderedSet[StateTransition]:
244
+ def _compute_exit_set(self, transitions: list[Transition]) -> OrderedSet[StateTransition]:
247
245
  """Compute the exit set for a transition."""
248
246
 
249
247
  states_to_exit = OrderedSet[StateTransition]()
@@ -286,7 +284,7 @@ class BaseEngine:
286
284
  return self.find_lcca([transition.source] + list(states))
287
285
 
288
286
  @staticmethod
289
- def find_lcca(states: List[State]) -> "State | None":
287
+ def find_lcca(states: list[State]) -> "State | None":
290
288
  """
291
289
  Find the Least Common Compound Ancestor (LCCA) of the given list of states.
292
290
 
@@ -371,7 +369,7 @@ class BaseEngine:
371
369
 
372
370
  return self._filter_conflicting_transitions(enabled_transitions)
373
371
 
374
- def microstep(self, transitions: List[Transition], trigger_data: TriggerData):
372
+ def microstep(self, transitions: list[Transition], trigger_data: TriggerData):
375
373
  """Process a single set of transitions in a 'lock step'.
376
374
  This includes exiting states, executing transition content, and entering states.
377
375
  """
@@ -454,7 +452,7 @@ class BaseEngine:
454
452
 
455
453
  def _prepare_exit_states(
456
454
  self,
457
- enabled_transitions: List[Transition],
455
+ enabled_transitions: list[Transition],
458
456
  ) -> "tuple[list[StateTransition], OrderedSet[State]]":
459
457
  """Compute exit set, sort, and update history. Pure computation, no callbacks."""
460
458
  states_to_exit = self._compute_exit_set(enabled_transitions)
@@ -491,7 +489,7 @@ class BaseEngine:
491
489
  self.sm._config.discard(state)
492
490
 
493
491
  def _exit_states(
494
- self, enabled_transitions: List[Transition], trigger_data: TriggerData
492
+ self, enabled_transitions: list[Transition], trigger_data: TriggerData
495
493
  ) -> OrderedSet[State]:
496
494
  """Compute and process the states to exit for the given transitions."""
497
495
  ordered_states, result = self._prepare_exit_states(enabled_transitions)
@@ -515,7 +513,7 @@ class BaseEngine:
515
513
 
516
514
  def _execute_transition_content(
517
515
  self,
518
- enabled_transitions: List[Transition],
516
+ enabled_transitions: list[Transition],
519
517
  trigger_data: TriggerData,
520
518
  get_key: Callable[[Transition], str],
521
519
  set_target_as_state: bool = False,
@@ -537,10 +535,10 @@ class BaseEngine:
537
535
 
538
536
  def _prepare_entry_states(
539
537
  self,
540
- enabled_transitions: List[Transition],
538
+ enabled_transitions: list[Transition],
541
539
  states_to_exit: OrderedSet[State],
542
540
  previous_configuration: OrderedSet[State],
543
- ) -> "tuple[list[StateTransition], OrderedSet[StateTransition], Dict[str, Any], OrderedSet[State]]": # noqa: E501
541
+ ) -> "tuple[list[StateTransition], OrderedSet[StateTransition], dict[str, Any], OrderedSet[State]]": # noqa: E501
544
542
  """Compute entry set, ordering, and new configuration. Pure computation, no callbacks.
545
543
 
546
544
  Returns:
@@ -548,7 +546,7 @@ class BaseEngine:
548
546
  """
549
547
  states_to_enter = OrderedSet[StateTransition]()
550
548
  states_for_default_entry = OrderedSet[StateTransition]()
551
- default_history_content: Dict[str, Any] = {}
549
+ default_history_content: dict[str, Any] = {}
552
550
 
553
551
  self.compute_entry_set(
554
552
  enabled_transitions, states_to_enter, states_for_default_entry, default_history_content
@@ -626,7 +624,7 @@ class BaseEngine:
626
624
 
627
625
  def _enter_states( # noqa: C901
628
626
  self,
629
- enabled_transitions: List[Transition],
627
+ enabled_transitions: list[Transition],
630
628
  trigger_data: TriggerData,
631
629
  states_to_exit: OrderedSet[State],
632
630
  previous_configuration: OrderedSet[State],
statemachine/event.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from typing import TYPE_CHECKING
2
2
  from typing import Any
3
- from typing import List
4
3
  from typing import cast
5
4
  from uuid import uuid4
6
5
 
@@ -189,7 +188,7 @@ class Event(AddCallbacksMixin, str):
189
188
 
190
189
  def split( # type: ignore[override]
191
190
  self, sep: "str | None" = None, maxsplit: int = -1
192
- ) -> List["Event"]:
191
+ ) -> list["Event"]:
193
192
  result = super().split(sep, maxsplit)
194
193
  if len(result) == 1:
195
194
  return [self]
@@ -1,5 +1,5 @@
1
+ from collections.abc import MutableSet
1
2
  from typing import TYPE_CHECKING
2
- from typing import MutableSet
3
3
 
4
4
  from .i18n import _
5
5
 
statemachine/factory.py CHANGED
@@ -1,9 +1,5 @@
1
1
  import re
2
2
  from typing import Any
3
- from typing import Dict
4
- from typing import List
5
- from typing import Optional
6
- from typing import Tuple
7
3
 
8
4
  from . import registry
9
5
  from .callbacks import CallbackGroup
@@ -39,8 +35,8 @@ class StateMachineMetaclass(type):
39
35
  def __init__(
40
36
  cls,
41
37
  name: str,
42
- bases: Tuple[type],
43
- attrs: Dict[str, Any],
38
+ bases: tuple[type],
39
+ attrs: dict[str, Any],
44
40
  ) -> None:
45
41
  super().__init__(name, bases, attrs)
46
42
  registry.register(cls)
@@ -49,13 +45,13 @@ class StateMachineMetaclass(type):
49
45
  # TODO: Experiment with the IDEA of a root state
50
46
  # cls.root = State(id=cls.id, name=cls.name)
51
47
  cls.states: States = States()
52
- cls.states_map: Dict[Any, State] = {}
48
+ cls.states_map: dict[Any, State] = {}
53
49
  """Map of ``state.value`` to the corresponding :ref:`state`."""
54
50
 
55
51
  cls._abstract = True
56
- cls._events: Dict[Event, None] = {} # used Dict to preserve order and avoid duplicates
52
+ cls._events: dict[Event, None] = {} # used Dict to preserve order and avoid duplicates
57
53
  cls._protected_attrs: set = set()
58
- cls._events_to_update: Dict[Event, Optional[Event]] = {}
54
+ cls._events_to_update: dict[Event, Event | None] = {}
59
55
  cls._specs = CallbackSpecList()
60
56
  cls.prepare = cls._specs.grouper(CallbackGroup.PREPARE).add(
61
57
  "prepare_event", priority=CallbackPriority.GENERIC, is_convention=True
@@ -88,7 +84,7 @@ class StateMachineMetaclass(type):
88
84
  else: # pragma: no cover
89
85
  cls.initial_state = None
90
86
 
91
- cls.final_states: List[State] = [state for state in cls.states if state.final]
87
+ cls.final_states: list[State] = [state for state in cls.states if state.final]
92
88
 
93
89
  cls._check()
94
90
  cls._setup()
@@ -99,7 +95,7 @@ class StateMachineMetaclass(type):
99
95
  def _expand_docstring(cls) -> None:
100
96
  """Replace ``{statechart:FORMAT}`` placeholders in the class docstring."""
101
97
  doc = cls.__doc__
102
- if not doc:
98
+ if not doc or not cls._STATECHART_RE.search(doc):
103
99
  return
104
100
 
105
101
  from .contrib.diagram.formatter import formatter
@@ -130,10 +126,10 @@ class StateMachineMetaclass(type):
130
126
  return formatter.render(cls, fmt) # type: ignore[arg-type]
131
127
 
132
128
  def _initials_by_document_order( # noqa: C901
133
- cls, states: List[State], parent: "State | None" = None, order: int = 1
129
+ cls, states: list[State], parent: "State | None" = None, order: int = 1
134
130
  ):
135
131
  """Set initial state by document order if no explicit initial state is set"""
136
- initials: List[State] = []
132
+ initials: list[State] = []
137
133
  for s in states:
138
134
  s.document_order = order
139
135
  order += 1
@@ -272,13 +268,13 @@ class StateMachineMetaclass(type):
272
268
  "send",
273
269
  } | {s.id for s in cls.states}
274
270
 
275
- def _collect_class_listeners(cls, attrs: Dict[str, Any], bases: Tuple[type]):
271
+ def _collect_class_listeners(cls, attrs: dict[str, Any], bases: tuple[type]):
276
272
  """Collect class-level listener declarations from attrs and MRO.
277
273
 
278
274
  Listeners declared on parent classes are prepended (MRO order),
279
275
  unless the child sets ``listeners_inherit = False``.
280
276
  """
281
- class_listeners: List[Any] = []
277
+ class_listeners: list[Any] = []
282
278
  if attrs.get("listeners_inherit", True):
283
279
  for base in reversed(bases):
284
280
  class_listeners.extend(getattr(base, "_class_listeners", []))
@@ -291,7 +287,7 @@ class StateMachineMetaclass(type):
291
287
  ).format(entry)
292
288
  )
293
289
  class_listeners.append(entry)
294
- cls._class_listeners: List[Any] = class_listeners
290
+ cls._class_listeners: list[Any] = class_listeners
295
291
 
296
292
  def add_inherited(cls, bases):
297
293
  for base in bases:
statemachine/graph.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from collections import deque
2
+ from collections.abc import Iterable
3
+ from collections.abc import MutableSet
2
4
  from typing import TYPE_CHECKING
3
- from typing import Iterable
4
- from typing import MutableSet
5
5
 
6
6
  if TYPE_CHECKING:
7
7
  from .state import State