python-statemachine 3.2.0__py3-none-any.whl → 3.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/METADATA +7 -6
- python_statemachine-3.2.2.dist-info/RECORD +73 -0
- {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/WHEEL +1 -1
- statemachine/__init__.py +1 -1
- statemachine/callbacks.py +11 -0
- statemachine/contrib/diagram/__init__.py +15 -6
- statemachine/contrib/diagram/extract.py +3 -3
- statemachine/contrib/diagram/formatter.py +4 -2
- statemachine/contrib/diagram/graphviz.py +66 -0
- statemachine/contrib/diagram/sphinx_ext.py +28 -7
- statemachine/dispatcher.py +8 -3
- statemachine/engines/async_.py +224 -180
- statemachine/engines/base.py +257 -245
- statemachine/engines/sync.py +224 -108
- statemachine/event.py +29 -10
- statemachine/event_data.py +19 -3
- statemachine/events.py +5 -2
- statemachine/exceptions.py +1 -1
- statemachine/factory.py +126 -29
- statemachine/io/__init__.py +18 -11
- statemachine/io/actions.py +124 -15
- statemachine/io/evaluators.py +37 -9
- statemachine/io/invoke.py +12 -0
- statemachine/io/loader.py +17 -3
- statemachine/io/schemas/statechart.schema.json +8 -1
- statemachine/io/scxml/__init__.py +6 -3
- statemachine/io/scxml/processor.py +1 -1
- statemachine/io/scxml/reader.py +24 -9
- statemachine/io/system_variables.py +34 -19
- statemachine/locale/en/LC_MESSAGES/statemachine.po +58 -17
- statemachine/locale/hi_IN/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/pt_BR/LC_MESSAGES/statemachine.po +66 -17
- statemachine/locale/zh_CN/LC_MESSAGES/statemachine.po +60 -17
- statemachine/orderedset.py +12 -3
- statemachine/registry.py +6 -1
- statemachine/spec_parser.py +151 -21
- statemachine/state.py +47 -8
- statemachine/statemachine.py +42 -19
- python_statemachine-3.2.0.dist-info/RECORD +0 -72
- {python_statemachine-3.2.0.dist-info → python_statemachine-3.2.2.dist-info}/licenses/LICENSE +0 -0
statemachine/engines/async_.py
CHANGED
|
@@ -1,30 +1,57 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import contextvars
|
|
3
2
|
from collections.abc import Callable
|
|
4
3
|
from itertools import chain
|
|
5
|
-
from time import
|
|
4
|
+
from time import monotonic
|
|
6
5
|
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
from ..event_data import EventData
|
|
9
8
|
from ..event_data import TriggerData
|
|
10
9
|
from ..exceptions import InvalidDefinition
|
|
10
|
+
from ..exceptions import StateMachineError
|
|
11
11
|
from ..exceptions import TransitionNotAllowed
|
|
12
|
+
from ..i18n import _
|
|
12
13
|
from ..orderedset import OrderedSet
|
|
13
14
|
from ..state import State
|
|
14
15
|
from .base import _ERROR_EXECUTION
|
|
15
16
|
from .base import BaseEngine
|
|
17
|
+
from .base import _processing_step
|
|
18
|
+
from .base import _running_engine
|
|
16
19
|
|
|
17
20
|
if TYPE_CHECKING:
|
|
21
|
+
from ..statemachine import StateChart
|
|
18
22
|
from ..transition import Transition
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
|
|
25
|
+
class _MacrostepCaller:
|
|
26
|
+
"""The caller of the external event whose macrostep is running, answered once the
|
|
27
|
+
macrostep ends, so that an exception raised by its internal events or eventless
|
|
28
|
+
transitions still reaches it."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.future: "asyncio.Future[object] | None" = None
|
|
32
|
+
self.result: object = None
|
|
33
|
+
|
|
34
|
+
def answer(self):
|
|
35
|
+
AsyncEngine._resolve_future(self.future, self.result)
|
|
36
|
+
self.future = None
|
|
37
|
+
self.result = None
|
|
38
|
+
|
|
39
|
+
def reject(self, exc: Exception) -> bool:
|
|
40
|
+
"""Reject the caller with ``exc``; returns whether a caller was still waiting."""
|
|
41
|
+
if self.future is None or self.future.done():
|
|
42
|
+
return False
|
|
43
|
+
self.future.set_exception(exc)
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def interrupt(self):
|
|
47
|
+
self.reject(
|
|
48
|
+
StateMachineError(
|
|
49
|
+
_(
|
|
50
|
+
"The task running the processing loop was cancelled while it processed "
|
|
51
|
+
"this event."
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
28
55
|
|
|
29
56
|
|
|
30
57
|
class AsyncEngine(BaseEngine):
|
|
@@ -34,23 +61,27 @@ class AsyncEngine(BaseEngine):
|
|
|
34
61
|
All pure-computation helpers are inherited from :class:`BaseEngine`.
|
|
35
62
|
"""
|
|
36
63
|
|
|
37
|
-
def
|
|
38
|
-
|
|
64
|
+
def __init__(self, sm: "StateChart"):
|
|
65
|
+
super().__init__(sm)
|
|
66
|
+
self._lock_waiters: "set[asyncio.Future[object]]" = set()
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
68
|
+
def put(self, trigger_data: TriggerData, internal: bool = False):
|
|
69
|
+
"""Override to attach an asyncio.Future to a trigger put from outside the loop.
|
|
70
|
+
|
|
71
|
+
Such a trigger always lands on the external queue, and its caller awaits the future
|
|
72
|
+
for its result. Futures are only created when:
|
|
42
73
|
- No future is already attached
|
|
43
74
|
- There is a running asyncio loop
|
|
44
75
|
- The call is NOT from within the processing loop (reentrant calls
|
|
45
76
|
from callbacks must not get futures, as that would deadlock)
|
|
46
77
|
"""
|
|
47
|
-
if
|
|
78
|
+
if trigger_data.future is None and _running_engine.get() is None:
|
|
48
79
|
try:
|
|
49
80
|
loop = asyncio.get_running_loop()
|
|
50
81
|
trigger_data.future = loop.create_future()
|
|
51
82
|
except RuntimeError:
|
|
52
83
|
pass # No running loop — sync caller
|
|
53
|
-
super().put(trigger_data, internal=internal
|
|
84
|
+
super().put(trigger_data, internal=internal)
|
|
54
85
|
|
|
55
86
|
@staticmethod
|
|
56
87
|
def _resolve_future(future: "asyncio.Future[object] | None", result):
|
|
@@ -58,12 +89,6 @@ class AsyncEngine(BaseEngine):
|
|
|
58
89
|
if future is not None and not future.done():
|
|
59
90
|
future.set_result(result)
|
|
60
91
|
|
|
61
|
-
@staticmethod
|
|
62
|
-
def _reject_future(future: "asyncio.Future[object] | None", exc: Exception):
|
|
63
|
-
"""Reject a future with the given exception, if present and not yet done."""
|
|
64
|
-
if future is not None and not future.done():
|
|
65
|
-
future.set_exception(exc)
|
|
66
|
-
|
|
67
92
|
def _reject_pending_futures(self, exc: Exception):
|
|
68
93
|
"""Reject all unresolved futures in the external queue."""
|
|
69
94
|
self.external_queue.reject_futures(exc)
|
|
@@ -71,17 +96,27 @@ class AsyncEngine(BaseEngine):
|
|
|
71
96
|
# --- Callback dispatch overrides (async versions of BaseEngine methods) ---
|
|
72
97
|
|
|
73
98
|
async def _get_args_kwargs(
|
|
74
|
-
self,
|
|
99
|
+
self,
|
|
100
|
+
transition: "Transition",
|
|
101
|
+
trigger_data: TriggerData,
|
|
102
|
+
target: "State | None" = None,
|
|
103
|
+
source: "State | None" = None,
|
|
75
104
|
):
|
|
76
|
-
cache_key = (id(transition), id(trigger_data), id(target))
|
|
105
|
+
cache_key = (id(transition), id(trigger_data), id(target), id(source))
|
|
77
106
|
|
|
78
107
|
if cache_key in self._cache:
|
|
79
108
|
return self._cache[cache_key]
|
|
80
109
|
|
|
81
110
|
event_data = EventData(trigger_data=trigger_data, transition=transition)
|
|
111
|
+
# See the sync engine for the rationale: bind `state`/`target` to the
|
|
112
|
+
# entered state and `state`/`source` to the exited state, keeping the
|
|
113
|
+
# generic enter/exit callbacks symmetric across compound boundaries.
|
|
82
114
|
if target:
|
|
83
115
|
event_data.state = target
|
|
84
116
|
event_data.target = target
|
|
117
|
+
if source:
|
|
118
|
+
event_data.state = source
|
|
119
|
+
event_data.source = source
|
|
85
120
|
|
|
86
121
|
args, kwargs = event_data.args, event_data.extended_kwargs
|
|
87
122
|
|
|
@@ -103,7 +138,7 @@ class AsyncEngine(BaseEngine):
|
|
|
103
138
|
transition.cond.key, *args, on_error=on_error, **kwargs
|
|
104
139
|
)
|
|
105
140
|
|
|
106
|
-
async def _first_transition_that_matches(
|
|
141
|
+
async def _first_transition_that_matches(
|
|
107
142
|
self,
|
|
108
143
|
state: State,
|
|
109
144
|
trigger_data: TriggerData,
|
|
@@ -120,7 +155,7 @@ class AsyncEngine(BaseEngine):
|
|
|
120
155
|
return transition
|
|
121
156
|
return None
|
|
122
157
|
|
|
123
|
-
async def _select_transitions(
|
|
158
|
+
async def _select_transitions(
|
|
124
159
|
self, trigger_data: TriggerData, predicate: Callable
|
|
125
160
|
) -> "OrderedSet[Transition]":
|
|
126
161
|
enabled_transitions: "OrderedSet[Transition]" = OrderedSet()
|
|
@@ -137,7 +172,7 @@ class AsyncEngine(BaseEngine):
|
|
|
137
172
|
async def select_eventless_transitions(self, trigger_data: TriggerData):
|
|
138
173
|
return await self._select_transitions(trigger_data, lambda t, _e: t.is_eventless)
|
|
139
174
|
|
|
140
|
-
async def select_transitions(self, trigger_data: TriggerData) -> "OrderedSet[Transition]":
|
|
175
|
+
async def select_transitions(self, trigger_data: TriggerData) -> "OrderedSet[Transition]":
|
|
141
176
|
return await self._select_transitions(trigger_data, lambda t, e: t.match(e))
|
|
142
177
|
|
|
143
178
|
async def _execute_transition_content(
|
|
@@ -162,7 +197,7 @@ class AsyncEngine(BaseEngine):
|
|
|
162
197
|
|
|
163
198
|
return result
|
|
164
199
|
|
|
165
|
-
async def _exit_states(
|
|
200
|
+
async def _exit_states(
|
|
166
201
|
self, enabled_transitions: "list[Transition]", trigger_data: TriggerData
|
|
167
202
|
) -> "OrderedSet[State]":
|
|
168
203
|
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
@@ -170,22 +205,22 @@ class AsyncEngine(BaseEngine):
|
|
|
170
205
|
|
|
171
206
|
for info in ordered_states:
|
|
172
207
|
# Cancel invocations for this state before executing exit handlers.
|
|
173
|
-
|
|
174
|
-
self._invoke_manager.cancel_for_state(info.state)
|
|
208
|
+
self._invoke_manager.cancel_for_state(info.state)
|
|
175
209
|
|
|
176
|
-
args, kwargs = await self._get_args_kwargs(
|
|
210
|
+
args, kwargs = await self._get_args_kwargs(
|
|
211
|
+
info.transition, trigger_data, source=info.state
|
|
212
|
+
)
|
|
177
213
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
)
|
|
214
|
+
self._debug("%s Exiting state: %s", self._log_id, info.state)
|
|
215
|
+
await self.sm._callbacks.async_call(
|
|
216
|
+
info.state.exit.key, *args, on_error=on_error, **kwargs
|
|
217
|
+
)
|
|
183
218
|
|
|
184
219
|
self._remove_state_from_configuration(info.state)
|
|
185
220
|
|
|
186
221
|
return result
|
|
187
222
|
|
|
188
|
-
async def _enter_states(
|
|
223
|
+
async def _enter_states(
|
|
189
224
|
self,
|
|
190
225
|
enabled_transitions: "list[Transition]",
|
|
191
226
|
trigger_data: TriggerData,
|
|
@@ -317,19 +352,6 @@ class AsyncEngine(BaseEngine):
|
|
|
317
352
|
|
|
318
353
|
# --- Engine loop ---
|
|
319
354
|
|
|
320
|
-
async def _run_microstep(self, enabled_transitions, trigger_data): # pragma: no cover
|
|
321
|
-
"""Run a microstep for internal/eventless transitions with error handling.
|
|
322
|
-
|
|
323
|
-
Note: microstep() handles its own errors internally, so this try/except
|
|
324
|
-
is a safety net that is not expected to be reached in normal operation.
|
|
325
|
-
"""
|
|
326
|
-
try:
|
|
327
|
-
await self.microstep(list(enabled_transitions), trigger_data)
|
|
328
|
-
except InvalidDefinition:
|
|
329
|
-
raise
|
|
330
|
-
except Exception as e:
|
|
331
|
-
self._handle_error(e, trigger_data)
|
|
332
|
-
|
|
333
355
|
async def activate_initial_state(self, **kwargs):
|
|
334
356
|
"""Activate the initial state.
|
|
335
357
|
|
|
@@ -342,162 +364,184 @@ class AsyncEngine(BaseEngine):
|
|
|
342
364
|
"""
|
|
343
365
|
return await self.processing_loop()
|
|
344
366
|
|
|
345
|
-
async def processing_loop(
|
|
346
|
-
|
|
347
|
-
):
|
|
348
|
-
"""Process event triggers with the 3-phase macrostep architecture.
|
|
367
|
+
async def processing_loop(self, caller_future: "asyncio.Future[object] | None" = None):
|
|
368
|
+
"""Process event triggers, a macrostep at a time.
|
|
349
369
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
370
|
+
Each macrostep takes eventless transitions and internal events until neither enables a
|
|
371
|
+
transition, spawns the invocations of the states it entered, and then takes the
|
|
372
|
+
external events that are due.
|
|
353
373
|
|
|
354
374
|
When ``caller_future`` is provided, the caller can ``await`` it to
|
|
355
375
|
receive its own event's result — even if another coroutine holds the
|
|
356
|
-
processing lock.
|
|
376
|
+
processing lock. If that coroutine is cancelled, the caller takes the loop over.
|
|
357
377
|
"""
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
378
|
+
while not self._processing.acquire(blocking=False):
|
|
379
|
+
if caller_future is None:
|
|
380
|
+
return None
|
|
381
|
+
await self._wait_for_result_or_release(caller_future)
|
|
382
|
+
if caller_future.done():
|
|
383
|
+
return caller_future.result()
|
|
384
|
+
|
|
385
|
+
try:
|
|
386
|
+
return await self._run_processing_loop(caller_future)
|
|
387
|
+
finally:
|
|
388
|
+
self._processing.release()
|
|
389
|
+
self._wake_lock_waiters()
|
|
390
|
+
|
|
391
|
+
async def _wait_for_result_or_release(self, caller_future: "asyncio.Future[object]"):
|
|
392
|
+
"""Wait until the caller's event is answered or the processing lock is released.
|
|
393
|
+
|
|
394
|
+
A caller cancelled here cancels its future, so its event, still processed, answers
|
|
395
|
+
nobody.
|
|
396
|
+
"""
|
|
397
|
+
released = caller_future.get_loop().create_future()
|
|
398
|
+
self._lock_waiters.add(released)
|
|
399
|
+
try:
|
|
400
|
+
await asyncio.wait((caller_future, released), return_when=asyncio.FIRST_COMPLETED)
|
|
401
|
+
except BaseException:
|
|
402
|
+
caller_future.cancel()
|
|
403
|
+
raise
|
|
404
|
+
finally:
|
|
405
|
+
self._lock_waiters.discard(released)
|
|
406
|
+
|
|
407
|
+
def _wake_lock_waiters(self):
|
|
408
|
+
for released in self._lock_waiters:
|
|
409
|
+
self._resolve_future(released, None)
|
|
364
410
|
|
|
365
|
-
|
|
411
|
+
async def _run_processing_loop(self, caller_future: "asyncio.Future[object] | None"):
|
|
366
412
|
self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
|
|
367
413
|
first_result = self._sentinel
|
|
414
|
+
macrostep = _MacrostepCaller()
|
|
368
415
|
try:
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
self.
|
|
372
|
-
took_events = False
|
|
373
|
-
macrostep_done = False
|
|
374
|
-
|
|
375
|
-
# Phase 1: eventless transitions and internal events
|
|
376
|
-
while not macrostep_done:
|
|
377
|
-
self._microstep_count = 0
|
|
378
|
-
self._debug(
|
|
379
|
-
"%s Macrostep %d: eventless/internal queue",
|
|
380
|
-
self._log_id,
|
|
381
|
-
self._macrostep_count,
|
|
382
|
-
)
|
|
383
|
-
|
|
416
|
+
with _processing_step(self):
|
|
417
|
+
took_events = True
|
|
418
|
+
while took_events and self.running:
|
|
384
419
|
self.clear_cache()
|
|
385
|
-
|
|
386
|
-
enabled_transitions = await self.select_eventless_transitions(internal_event)
|
|
387
|
-
if not enabled_transitions:
|
|
388
|
-
if self.internal_queue.is_empty():
|
|
389
|
-
macrostep_done = True
|
|
390
|
-
else:
|
|
391
|
-
internal_event = self.internal_queue.pop()
|
|
392
|
-
enabled_transitions = await self.select_transitions(internal_event)
|
|
393
|
-
if enabled_transitions:
|
|
394
|
-
self._debug(
|
|
395
|
-
"%s Enabled transitions: %s", self._log_id, enabled_transitions
|
|
396
|
-
)
|
|
397
|
-
took_events = True
|
|
398
|
-
await self._run_microstep(enabled_transitions, internal_event)
|
|
399
|
-
|
|
400
|
-
# Spawn invoke handlers for states entered during this macrostep.
|
|
401
|
-
await self._invoke_manager.spawn_pending_async()
|
|
402
|
-
self._check_root_final_state()
|
|
403
|
-
|
|
404
|
-
# Phase 2: remaining internal events
|
|
405
|
-
while not self.internal_queue.is_empty(): # pragma: no cover
|
|
406
|
-
internal_event = self.internal_queue.pop()
|
|
407
|
-
enabled_transitions = await self.select_transitions(internal_event)
|
|
408
|
-
if enabled_transitions:
|
|
409
|
-
await self._run_microstep(enabled_transitions, internal_event)
|
|
410
|
-
|
|
411
|
-
# Phase 3: external events
|
|
412
|
-
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
|
|
413
|
-
while not self.external_queue.is_empty():
|
|
414
|
-
self.clear_cache()
|
|
415
|
-
took_events = True
|
|
416
|
-
external_event = self.external_queue.pop()
|
|
417
|
-
current_time = time()
|
|
418
|
-
if external_event.execution_time > current_time:
|
|
419
|
-
self.put(external_event, _delayed=True)
|
|
420
|
-
await asyncio.sleep(self.sm._loop_sleep_in_ms)
|
|
421
|
-
# Break to Phase 1 so internal events and eventless
|
|
422
|
-
# transitions can be processed while we wait.
|
|
423
|
-
break
|
|
420
|
+
took_events = await self._run_macrostep()
|
|
424
421
|
|
|
425
|
-
|
|
426
|
-
self.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
self._macrostep_count,
|
|
431
|
-
external_event.event,
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
# Handle lazy initial state activation.
|
|
435
|
-
# Break out of phase 3 so the outer loop restarts from phase 1
|
|
436
|
-
# (eventless/internal), ensuring internal events queued during
|
|
437
|
-
# initial entry are processed before any external events.
|
|
438
|
-
if external_event.event == "__initial__":
|
|
439
|
-
transitions = self._initial_transitions(external_event)
|
|
440
|
-
await self._enter_states(
|
|
441
|
-
transitions, external_event, OrderedSet(), OrderedSet()
|
|
442
|
-
)
|
|
422
|
+
# Spawn invoke handlers for states entered during this macrostep.
|
|
423
|
+
await self._invoke_manager.spawn_pending_async()
|
|
424
|
+
macrostep.answer()
|
|
425
|
+
self._check_root_final_state()
|
|
426
|
+
if not self.running:
|
|
443
427
|
break
|
|
444
428
|
|
|
445
|
-
|
|
446
|
-
|
|
429
|
+
took_external_events, first_result = await self._take_due_external_event(
|
|
430
|
+
first_result, macrostep
|
|
431
|
+
)
|
|
432
|
+
took_events = took_events or took_external_events
|
|
447
433
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
self._debug(
|
|
452
|
-
"%s Enabled transitions: %s", self._log_id, enabled_transitions
|
|
453
|
-
)
|
|
454
|
-
if enabled_transitions:
|
|
455
|
-
result = await self.microstep(
|
|
456
|
-
list(enabled_transitions), external_event
|
|
457
|
-
)
|
|
458
|
-
self._resolve_future(event_future, result)
|
|
459
|
-
if first_result is self._sentinel:
|
|
460
|
-
first_result = result
|
|
461
|
-
else:
|
|
462
|
-
if not self.sm.allow_event_without_transition:
|
|
463
|
-
tna = TransitionNotAllowed(
|
|
464
|
-
external_event.event, self.sm.configuration
|
|
465
|
-
)
|
|
466
|
-
self._reject_future(event_future, tna)
|
|
467
|
-
self._reject_pending_futures(tna)
|
|
468
|
-
raise tna
|
|
469
|
-
# Event allowed but no transition — resolve with None
|
|
470
|
-
self._resolve_future(event_future, None)
|
|
471
|
-
except Exception as exc:
|
|
472
|
-
self._reject_future(event_future, exc)
|
|
473
|
-
self._reject_pending_futures(exc)
|
|
474
|
-
self.clear()
|
|
475
|
-
raise
|
|
434
|
+
macrostep.answer()
|
|
435
|
+
if not self.running:
|
|
436
|
+
self._drop_external_events()
|
|
476
437
|
|
|
477
438
|
except Exception as exc:
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
# get their successful result via ``await future`` below, and
|
|
483
|
-
# the failing event's exception was already routed to *its*
|
|
484
|
-
# caller's future by ``_reject_future(event_future, ...)``.
|
|
485
|
-
self._reject_future(caller_future, exc)
|
|
486
|
-
else:
|
|
439
|
+
answered = caller_future is None or caller_future.done()
|
|
440
|
+
self._reject_pending_futures(exc)
|
|
441
|
+
self.clear()
|
|
442
|
+
if not macrostep.reject(exc) and answered:
|
|
487
443
|
raise
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
444
|
+
except BaseException:
|
|
445
|
+
self.internal_queue.clear()
|
|
446
|
+
macrostep.interrupt()
|
|
447
|
+
if caller_future is not None:
|
|
448
|
+
caller_future.cancel()
|
|
449
|
+
raise
|
|
491
450
|
|
|
492
451
|
self._debug("%s Processing loop ended", self._log_id)
|
|
493
452
|
result = first_result if first_result is not self._sentinel else None
|
|
494
|
-
# If the caller has a future, await it (already resolved by now).
|
|
495
453
|
if caller_future is not None:
|
|
496
454
|
# Resolve the future if it wasn't processed (e.g. machine terminated).
|
|
497
455
|
self._resolve_future(caller_future, result)
|
|
498
456
|
return await caller_future
|
|
499
457
|
return result
|
|
500
458
|
|
|
459
|
+
async def _run_macrostep(self) -> bool:
|
|
460
|
+
"""Take eventless transitions and internal events until neither enables a transition.
|
|
461
|
+
|
|
462
|
+
Returns whether any transition was taken. The internal queue is empty on return.
|
|
463
|
+
"""
|
|
464
|
+
took_events = False
|
|
465
|
+
while True:
|
|
466
|
+
self._microstep_count = 0
|
|
467
|
+
self._debug(
|
|
468
|
+
"%s Macrostep %d: eventless/internal queue",
|
|
469
|
+
self._log_id,
|
|
470
|
+
self._macrostep_count,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
self.clear_cache()
|
|
474
|
+
internal_event = TriggerData(self.sm, event=None) # null object for eventless
|
|
475
|
+
enabled_transitions = await self.select_eventless_transitions(internal_event)
|
|
476
|
+
if not enabled_transitions:
|
|
477
|
+
if self.internal_queue.is_empty():
|
|
478
|
+
return took_events
|
|
479
|
+
internal_event = self.internal_queue.pop()
|
|
480
|
+
enabled_transitions = await self.select_transitions(internal_event)
|
|
481
|
+
if enabled_transitions:
|
|
482
|
+
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
|
|
483
|
+
took_events = True
|
|
484
|
+
await self.microstep(list(enabled_transitions), internal_event)
|
|
485
|
+
|
|
486
|
+
async def _take_due_external_event(self, first_result, macrostep: "_MacrostepCaller"):
|
|
487
|
+
"""Take the external event at the head of the queue, if it is due.
|
|
488
|
+
|
|
489
|
+
One at a time, so the macrostep that follows runs the internal events and eventless
|
|
490
|
+
transitions it enabled before the next external event is taken. The event's caller is
|
|
491
|
+
left in ``macrostep``, to be answered once that macrostep ends.
|
|
492
|
+
|
|
493
|
+
Returns whether the queue held an event, and ``first_result`` updated with the result
|
|
494
|
+
of the first event that enabled a transition.
|
|
495
|
+
"""
|
|
496
|
+
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
|
|
497
|
+
if self.external_queue.is_empty():
|
|
498
|
+
return False, first_result
|
|
499
|
+
|
|
500
|
+
self.clear_cache()
|
|
501
|
+
external_event = self.external_queue.pop_due(monotonic())
|
|
502
|
+
if external_event is None:
|
|
503
|
+
await asyncio.sleep(self.sm._loop_sleep_in_ms)
|
|
504
|
+
return True, first_result
|
|
505
|
+
|
|
506
|
+
self._macrostep_count += 1
|
|
507
|
+
self._microstep_count = 0
|
|
508
|
+
self._debug(
|
|
509
|
+
"%s macrostep %d: event=%s",
|
|
510
|
+
self._log_id,
|
|
511
|
+
self._macrostep_count,
|
|
512
|
+
external_event.event,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Handle lazy initial state activation.
|
|
516
|
+
if external_event.event == "__initial__":
|
|
517
|
+
transitions = self._initial_transitions(external_event)
|
|
518
|
+
await self._enter_states(transitions, external_event, OrderedSet(), OrderedSet())
|
|
519
|
+
return True, first_result
|
|
520
|
+
|
|
521
|
+
macrostep.future = external_event.future
|
|
522
|
+
result = await self._take_external_event(external_event)
|
|
523
|
+
if result is not self._sentinel:
|
|
524
|
+
macrostep.result = result
|
|
525
|
+
if first_result is self._sentinel:
|
|
526
|
+
first_result = result
|
|
527
|
+
return True, first_result
|
|
528
|
+
|
|
529
|
+
async def _take_external_event(self, external_event: TriggerData):
|
|
530
|
+
"""Run one external event.
|
|
531
|
+
|
|
532
|
+
Returns the event's microstep result, or the sentinel when it enabled no transition.
|
|
533
|
+
"""
|
|
534
|
+
# Finalize + autoforward for active invocations
|
|
535
|
+
self._invoke_manager.handle_external_event(external_event)
|
|
536
|
+
|
|
537
|
+
enabled_transitions = await self.select_transitions(external_event)
|
|
538
|
+
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
|
|
539
|
+
if enabled_transitions:
|
|
540
|
+
return await self.microstep(list(enabled_transitions), external_event)
|
|
541
|
+
if not self.sm.allow_event_without_transition:
|
|
542
|
+
raise TransitionNotAllowed(external_event.event, self.sm.configuration)
|
|
543
|
+
return self._sentinel
|
|
544
|
+
|
|
501
545
|
async def enabled_events(self, *args, **kwargs):
|
|
502
546
|
sm = self.sm
|
|
503
547
|
enabled = {}
|