python-statemachine 3.1.2__py3-none-any.whl → 3.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/METADATA +16 -3
- python_statemachine-3.2.1.dist-info/RECORD +72 -0
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +5 -11
- statemachine/configuration.py +5 -6
- statemachine/contrib/diagram/extract.py +26 -27
- statemachine/contrib/diagram/formatter.py +5 -7
- statemachine/contrib/diagram/model.py +9 -11
- statemachine/contrib/diagram/renderers/dot.py +20 -26
- statemachine/contrib/diagram/renderers/mermaid.py +36 -40
- statemachine/contrib/diagram/renderers/table.py +7 -9
- statemachine/contrib/weighted.py +7 -11
- statemachine/dispatcher.py +18 -13
- statemachine/engines/async_.py +20 -9
- statemachine/engines/base.py +26 -17
- statemachine/event.py +1 -2
- statemachine/exceptions.py +2 -2
- statemachine/factory.py +11 -15
- statemachine/graph.py +2 -2
- statemachine/invoke.py +12 -11
- statemachine/io/__init__.py +52 -225
- statemachine/io/actions.py +627 -0
- statemachine/io/builder.py +195 -0
- statemachine/io/class_factory.py +236 -0
- statemachine/io/evaluators.py +303 -0
- statemachine/io/interpreter.py +128 -0
- statemachine/io/{scxml/invoke.py → invoke.py} +89 -49
- statemachine/io/json/__init__.py +1 -0
- statemachine/io/json/reader.py +27 -0
- statemachine/io/loader.py +175 -0
- statemachine/io/model.py +268 -0
- statemachine/io/native.py +402 -0
- statemachine/io/ports.py +83 -0
- statemachine/io/schemas/statechart.schema.json +258 -0
- statemachine/io/scxml/__init__.py +15 -0
- statemachine/io/scxml/processor.py +23 -253
- statemachine/io/scxml/{parser.py → reader.py} +88 -56
- statemachine/io/system_variables.py +199 -0
- statemachine/io/validation.py +44 -0
- statemachine/io/yaml/__init__.py +1 -0
- statemachine/io/yaml/reader.py +65 -0
- statemachine/locale/en/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +19 -19
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +19 -19
- statemachine/orderedset.py +15 -6
- statemachine/registry.py +1 -4
- statemachine/signature.py +2 -5
- statemachine/spec_parser.py +228 -45
- statemachine/state.py +7 -8
- statemachine/statemachine.py +18 -20
- statemachine/states.py +3 -5
- statemachine/transition.py +3 -4
- statemachine/transition_list.py +4 -5
- statemachine/transition_mixin.py +1 -1
- python_statemachine-3.1.2.dist-info/RECORD +0 -58
- statemachine/io/scxml/actions.py +0 -650
- statemachine/io/scxml/schema.py +0 -175
- {python_statemachine-3.1.2.dist-info → python_statemachine-3.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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:
|
|
33
|
+
def __init__(self, config: MermaidRendererConfig | None = None):
|
|
38
34
|
self.config = config or MermaidRendererConfig()
|
|
39
|
-
self._active_ids:
|
|
40
|
-
self._rendered_transitions:
|
|
41
|
-
self._compound_ids:
|
|
42
|
-
self._initial_child_map:
|
|
43
|
-
self._parallel_descendant_ids:
|
|
44
|
-
self._all_descendants_map:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
82
|
+
states: list[DiagramState],
|
|
87
83
|
inside_parallel: bool = False,
|
|
88
|
-
) ->
|
|
84
|
+
) -> set[str]:
|
|
89
85
|
"""Collect IDs of all states that are descendants of a parallel state."""
|
|
90
|
-
result:
|
|
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:
|
|
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:
|
|
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:
|
|
106
|
+
def _collect_recursive_descendants(states: list[DiagramState]) -> set[str]:
|
|
111
107
|
"""Collect all state IDs in a subtree recursively."""
|
|
112
|
-
ids:
|
|
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:
|
|
136
|
-
transitions:
|
|
137
|
-
lines:
|
|
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:
|
|
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:
|
|
190
|
-
lines:
|
|
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:
|
|
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:
|
|
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:
|
|
240
|
-
scope_ids:
|
|
241
|
-
lines:
|
|
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:
|
|
261
|
-
expanded:
|
|
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:
|
|
299
|
-
compound_descendants:
|
|
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:
|
|
308
|
+
lines: list[str],
|
|
313
309
|
indent: int,
|
|
314
310
|
) -> None:
|
|
315
311
|
pad = " " * indent
|
|
316
|
-
label_parts:
|
|
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:
|
|
337
|
-
lines:
|
|
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:
|
|
30
|
-
transitions:
|
|
31
|
-
) -> "
|
|
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:
|
|
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:
|
|
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: "
|
|
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: "
|
|
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]
|
statemachine/contrib/weighted.py
CHANGED
|
@@ -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
|
|
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:
|
|
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 =
|
|
64
|
-
|
|
65
|
-
|
|
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) -> "
|
|
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:
|
|
108
|
+
kwargs: dict[str, Any] = {}
|
|
113
109
|
else:
|
|
114
110
|
target, weight, kwargs = item
|
|
115
111
|
if not isinstance(kwargs, dict):
|
statemachine/dispatcher.py
CHANGED
|
@@ -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
|
|
@@ -17,6 +14,7 @@ from .event import Event
|
|
|
17
14
|
from .exceptions import InvalidDefinition
|
|
18
15
|
from .i18n import _
|
|
19
16
|
from .signature import SignatureAdapter
|
|
17
|
+
from .spec_parser import UnsupportedExpression
|
|
20
18
|
from .spec_parser import custom_and
|
|
21
19
|
from .spec_parser import operator_mapping
|
|
22
20
|
from .spec_parser import parse_boolean_expr
|
|
@@ -37,7 +35,7 @@ class Listener:
|
|
|
37
35
|
"""
|
|
38
36
|
|
|
39
37
|
obj: object
|
|
40
|
-
all_attrs:
|
|
38
|
+
all_attrs: set[str]
|
|
41
39
|
resolver_id: str
|
|
42
40
|
|
|
43
41
|
@classmethod
|
|
@@ -58,8 +56,8 @@ class Listener:
|
|
|
58
56
|
class Listeners:
|
|
59
57
|
"""Listeners that provides attributes to be used as callbacks."""
|
|
60
58
|
|
|
61
|
-
items:
|
|
62
|
-
all_attrs:
|
|
59
|
+
items: tuple[Listener, ...]
|
|
60
|
+
all_attrs: set[str]
|
|
63
61
|
|
|
64
62
|
@classmethod
|
|
65
63
|
def from_listeners(cls, listeners: Iterable["Listener"]) -> "Listeners":
|
|
@@ -86,7 +84,7 @@ class Listeners:
|
|
|
86
84
|
executor.add(key, spec, builder)
|
|
87
85
|
|
|
88
86
|
def _take_callback(self, name: str, names_not_found_handler: Callable) -> Callable:
|
|
89
|
-
callbacks:
|
|
87
|
+
callbacks: list[Callable] = []
|
|
90
88
|
for key, builder in self.search_name(name):
|
|
91
89
|
callback = builder()
|
|
92
90
|
callback.unique_key = key # type: ignore[attr-defined]
|
|
@@ -114,14 +112,17 @@ class Listeners:
|
|
|
114
112
|
|
|
115
113
|
# Resolves boolean expressions
|
|
116
114
|
|
|
117
|
-
names_not_found:
|
|
115
|
+
names_not_found: set[str] = set()
|
|
118
116
|
take_callback_partial = partial(
|
|
119
117
|
self._take_callback, names_not_found_handler=names_not_found.add
|
|
120
118
|
)
|
|
121
119
|
|
|
122
120
|
try:
|
|
123
121
|
expression = parse_boolean_expr(spec.func, take_callback_partial, operator_mapping)
|
|
124
|
-
except SyntaxError as err:
|
|
122
|
+
except (SyntaxError, UnsupportedExpression) as err:
|
|
123
|
+
# ``UnsupportedExpression`` comes from the AST allowlist rejecting a node
|
|
124
|
+
# kind that cannot appear in a boolean expression (e.g. ``"user.age"``).
|
|
125
|
+
# Errors raised while resolving names are not parse errors and propagate.
|
|
125
126
|
raise InvalidDefinition(
|
|
126
127
|
_("Failed to parse boolean expression '{}'").format(spec.func)
|
|
127
128
|
) from err
|
|
@@ -162,7 +163,11 @@ class Listeners:
|
|
|
162
163
|
if not spec.is_bounded:
|
|
163
164
|
for listener in self.items:
|
|
164
165
|
func = getattr(listener.obj, spec.attr_name, None)
|
|
165
|
-
|
|
166
|
+
# ``getattr`` may return a non-method that happens to share the name
|
|
167
|
+
# (e.g. a model attribute named like a compiled guard); it is not the
|
|
168
|
+
# unbounded method we are rebinding, so skip it instead of accessing
|
|
169
|
+
# ``__func__`` (which would raise on a plain value).
|
|
170
|
+
if getattr(func, "__func__", None) is spec.func:
|
|
166
171
|
yield listener.build_key(spec.attr_name), partial(callable_method, func)
|
|
167
172
|
return
|
|
168
173
|
|
|
@@ -228,5 +233,5 @@ def event_method(func) -> Callable:
|
|
|
228
233
|
return method
|
|
229
234
|
|
|
230
235
|
|
|
231
|
-
def resolver_factory_from_objects(*objects:
|
|
236
|
+
def resolver_factory_from_objects(*objects: tuple[Any, ...]):
|
|
232
237
|
return Listeners.from_listeners(Listener.from_obj(o) for o in objects)
|
statemachine/engines/async_.py
CHANGED
|
@@ -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
|
|
@@ -72,17 +71,27 @@ class AsyncEngine(BaseEngine):
|
|
|
72
71
|
# --- Callback dispatch overrides (async versions of BaseEngine methods) ---
|
|
73
72
|
|
|
74
73
|
async def _get_args_kwargs(
|
|
75
|
-
self,
|
|
74
|
+
self,
|
|
75
|
+
transition: "Transition",
|
|
76
|
+
trigger_data: TriggerData,
|
|
77
|
+
target: "State | None" = None,
|
|
78
|
+
source: "State | None" = None,
|
|
76
79
|
):
|
|
77
|
-
cache_key = (id(transition), id(trigger_data), id(target))
|
|
80
|
+
cache_key = (id(transition), id(trigger_data), id(target), id(source))
|
|
78
81
|
|
|
79
82
|
if cache_key in self._cache:
|
|
80
83
|
return self._cache[cache_key]
|
|
81
84
|
|
|
82
85
|
event_data = EventData(trigger_data=trigger_data, transition=transition)
|
|
86
|
+
# See the sync engine for the rationale: bind `state`/`target` to the
|
|
87
|
+
# entered state and `state`/`source` to the exited state, keeping the
|
|
88
|
+
# generic enter/exit callbacks symmetric across compound boundaries.
|
|
83
89
|
if target:
|
|
84
90
|
event_data.state = target
|
|
85
91
|
event_data.target = target
|
|
92
|
+
if source:
|
|
93
|
+
event_data.state = source
|
|
94
|
+
event_data.source = source
|
|
86
95
|
|
|
87
96
|
args, kwargs = event_data.args, event_data.extended_kwargs
|
|
88
97
|
|
|
@@ -143,7 +152,7 @@ class AsyncEngine(BaseEngine):
|
|
|
143
152
|
|
|
144
153
|
async def _execute_transition_content(
|
|
145
154
|
self,
|
|
146
|
-
enabled_transitions: "
|
|
155
|
+
enabled_transitions: "list[Transition]",
|
|
147
156
|
trigger_data: TriggerData,
|
|
148
157
|
get_key: "Callable[[Transition], str]",
|
|
149
158
|
set_target_as_state: bool = False,
|
|
@@ -164,7 +173,7 @@ class AsyncEngine(BaseEngine):
|
|
|
164
173
|
return result
|
|
165
174
|
|
|
166
175
|
async def _exit_states( # type: ignore[override]
|
|
167
|
-
self, enabled_transitions: "
|
|
176
|
+
self, enabled_transitions: "list[Transition]", trigger_data: TriggerData
|
|
168
177
|
) -> "OrderedSet[State]":
|
|
169
178
|
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
170
179
|
on_error = self._on_error_handler()
|
|
@@ -174,7 +183,9 @@ class AsyncEngine(BaseEngine):
|
|
|
174
183
|
if info.state is not None: # pragma: no branch
|
|
175
184
|
self._invoke_manager.cancel_for_state(info.state)
|
|
176
185
|
|
|
177
|
-
args, kwargs = await self._get_args_kwargs(
|
|
186
|
+
args, kwargs = await self._get_args_kwargs(
|
|
187
|
+
info.transition, trigger_data, source=info.state
|
|
188
|
+
)
|
|
178
189
|
|
|
179
190
|
if info.state is not None: # pragma: no branch
|
|
180
191
|
self._debug("%s Exiting state: %s", self._log_id, info.state)
|
|
@@ -188,7 +199,7 @@ class AsyncEngine(BaseEngine):
|
|
|
188
199
|
|
|
189
200
|
async def _enter_states( # noqa: C901
|
|
190
201
|
self,
|
|
191
|
-
enabled_transitions: "
|
|
202
|
+
enabled_transitions: "list[Transition]",
|
|
192
203
|
trigger_data: TriggerData,
|
|
193
204
|
states_to_exit: "OrderedSet[State]",
|
|
194
205
|
previous_configuration: "OrderedSet[State]",
|
|
@@ -270,7 +281,7 @@ class AsyncEngine(BaseEngine):
|
|
|
270
281
|
|
|
271
282
|
return result
|
|
272
283
|
|
|
273
|
-
async def microstep(self, transitions: "
|
|
284
|
+
async def microstep(self, transitions: "list[Transition]", trigger_data: TriggerData):
|
|
274
285
|
self._microstep_count += 1
|
|
275
286
|
self._debug(
|
|
276
287
|
"%s macro:%d micro:%d transitions: %s",
|
statemachine/engines/base.py
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
"""
|
|
@@ -421,19 +419,30 @@ class BaseEngine:
|
|
|
421
419
|
return result
|
|
422
420
|
|
|
423
421
|
def _get_args_kwargs(
|
|
424
|
-
self,
|
|
422
|
+
self,
|
|
423
|
+
transition: Transition,
|
|
424
|
+
trigger_data: TriggerData,
|
|
425
|
+
target: "State | None" = None,
|
|
426
|
+
source: "State | None" = None,
|
|
425
427
|
):
|
|
426
428
|
# Generate a unique key for the cache, the cache is invalidated once per loop
|
|
427
|
-
cache_key = (id(transition), id(trigger_data), id(target))
|
|
429
|
+
cache_key = (id(transition), id(trigger_data), id(target), id(source))
|
|
428
430
|
|
|
429
431
|
# Check the cache for existing results
|
|
430
432
|
if cache_key in self._cache:
|
|
431
433
|
return self._cache[cache_key]
|
|
432
434
|
|
|
433
435
|
event_data = EventData(trigger_data=trigger_data, transition=transition)
|
|
436
|
+
# Bind `state`/`target` to the individual state being entered, and
|
|
437
|
+
# `state`/`source` to the individual state being exited, so the generic
|
|
438
|
+
# `on_enter_state`/`on_exit_state` callbacks stay symmetric when crossing
|
|
439
|
+
# compound state boundaries (the transition only knows its own endpoints).
|
|
434
440
|
if target:
|
|
435
441
|
event_data.state = target
|
|
436
442
|
event_data.target = target
|
|
443
|
+
if source:
|
|
444
|
+
event_data.state = source
|
|
445
|
+
event_data.source = source
|
|
437
446
|
|
|
438
447
|
args, kwargs = event_data.args, event_data.extended_kwargs
|
|
439
448
|
|
|
@@ -454,7 +463,7 @@ class BaseEngine:
|
|
|
454
463
|
|
|
455
464
|
def _prepare_exit_states(
|
|
456
465
|
self,
|
|
457
|
-
enabled_transitions:
|
|
466
|
+
enabled_transitions: list[Transition],
|
|
458
467
|
) -> "tuple[list[StateTransition], OrderedSet[State]]":
|
|
459
468
|
"""Compute exit set, sort, and update history. Pure computation, no callbacks."""
|
|
460
469
|
states_to_exit = self._compute_exit_set(enabled_transitions)
|
|
@@ -491,7 +500,7 @@ class BaseEngine:
|
|
|
491
500
|
self.sm._config.discard(state)
|
|
492
501
|
|
|
493
502
|
def _exit_states(
|
|
494
|
-
self, enabled_transitions:
|
|
503
|
+
self, enabled_transitions: list[Transition], trigger_data: TriggerData
|
|
495
504
|
) -> OrderedSet[State]:
|
|
496
505
|
"""Compute and process the states to exit for the given transitions."""
|
|
497
506
|
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
@@ -502,7 +511,7 @@ class BaseEngine:
|
|
|
502
511
|
if info.state is not None: # pragma: no branch
|
|
503
512
|
self._invoke_manager.cancel_for_state(info.state)
|
|
504
513
|
|
|
505
|
-
args, kwargs = self._get_args_kwargs(info.transition, trigger_data)
|
|
514
|
+
args, kwargs = self._get_args_kwargs(info.transition, trigger_data, source=info.state)
|
|
506
515
|
|
|
507
516
|
# Execute `onexit` handlers — same per-block error isolation as onentry.
|
|
508
517
|
if info.state is not None: # pragma: no branch
|
|
@@ -515,7 +524,7 @@ class BaseEngine:
|
|
|
515
524
|
|
|
516
525
|
def _execute_transition_content(
|
|
517
526
|
self,
|
|
518
|
-
enabled_transitions:
|
|
527
|
+
enabled_transitions: list[Transition],
|
|
519
528
|
trigger_data: TriggerData,
|
|
520
529
|
get_key: Callable[[Transition], str],
|
|
521
530
|
set_target_as_state: bool = False,
|
|
@@ -537,10 +546,10 @@ class BaseEngine:
|
|
|
537
546
|
|
|
538
547
|
def _prepare_entry_states(
|
|
539
548
|
self,
|
|
540
|
-
enabled_transitions:
|
|
549
|
+
enabled_transitions: list[Transition],
|
|
541
550
|
states_to_exit: OrderedSet[State],
|
|
542
551
|
previous_configuration: OrderedSet[State],
|
|
543
|
-
) -> "tuple[list[StateTransition], OrderedSet[StateTransition],
|
|
552
|
+
) -> "tuple[list[StateTransition], OrderedSet[StateTransition], dict[str, Any], OrderedSet[State]]": # noqa: E501
|
|
544
553
|
"""Compute entry set, ordering, and new configuration. Pure computation, no callbacks.
|
|
545
554
|
|
|
546
555
|
Returns:
|
|
@@ -548,7 +557,7 @@ class BaseEngine:
|
|
|
548
557
|
"""
|
|
549
558
|
states_to_enter = OrderedSet[StateTransition]()
|
|
550
559
|
states_for_default_entry = OrderedSet[StateTransition]()
|
|
551
|
-
default_history_content:
|
|
560
|
+
default_history_content: dict[str, Any] = {}
|
|
552
561
|
|
|
553
562
|
self.compute_entry_set(
|
|
554
563
|
enabled_transitions, states_to_enter, states_for_default_entry, default_history_content
|
|
@@ -626,7 +635,7 @@ class BaseEngine:
|
|
|
626
635
|
|
|
627
636
|
def _enter_states( # noqa: C901
|
|
628
637
|
self,
|
|
629
|
-
enabled_transitions:
|
|
638
|
+
enabled_transitions: list[Transition],
|
|
630
639
|
trigger_data: TriggerData,
|
|
631
640
|
states_to_exit: OrderedSet[State],
|
|
632
641
|
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
|
-
) ->
|
|
191
|
+
) -> list["Event"]:
|
|
193
192
|
result = super().split(sep, maxsplit)
|
|
194
193
|
if len(result) == 1:
|
|
195
194
|
return [self]
|
statemachine/exceptions.py
CHANGED
|
@@ -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
|
|
|
@@ -36,7 +36,7 @@ class TransitionNotAllowed(StateMachineError):
|
|
|
36
36
|
def __init__(self, event: "Event | None", configuration: MutableSet["State"]):
|
|
37
37
|
self.event = event
|
|
38
38
|
self.configuration = configuration
|
|
39
|
-
name = ", ".join(
|
|
39
|
+
name = ", ".join(str(s.name) for s in configuration)
|
|
40
40
|
msg = _("Can't {} when in {}.").format(
|
|
41
41
|
self.event and self.event.name or "transition", name
|
|
42
42
|
)
|