python-statemachine 3.2.1__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.1.dist-info → python_statemachine-3.2.2.dist-info}/METADATA +7 -6
- {python_statemachine-3.2.1.dist-info → python_statemachine-3.2.2.dist-info}/RECORD +28 -27
- {python_statemachine-3.2.1.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/formatter.py +4 -2
- statemachine/contrib/diagram/graphviz.py +66 -0
- statemachine/contrib/diagram/sphinx_ext.py +28 -7
- statemachine/dispatcher.py +3 -2
- statemachine/engines/async_.py +209 -177
- statemachine/engines/base.py +245 -244
- statemachine/engines/sync.py +224 -108
- statemachine/event.py +29 -10
- statemachine/event_data.py +19 -3
- statemachine/events.py +5 -2
- statemachine/factory.py +126 -29
- statemachine/io/actions.py +4 -2
- statemachine/io/schemas/statechart.schema.json +8 -1
- 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/registry.py +6 -1
- statemachine/spec_parser.py +95 -19
- statemachine/state.py +45 -6
- statemachine/statemachine.py +42 -19
- {python_statemachine-3.2.1.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)
|
|
@@ -113,7 +138,7 @@ class AsyncEngine(BaseEngine):
|
|
|
113
138
|
transition.cond.key, *args, on_error=on_error, **kwargs
|
|
114
139
|
)
|
|
115
140
|
|
|
116
|
-
async def _first_transition_that_matches(
|
|
141
|
+
async def _first_transition_that_matches(
|
|
117
142
|
self,
|
|
118
143
|
state: State,
|
|
119
144
|
trigger_data: TriggerData,
|
|
@@ -130,7 +155,7 @@ class AsyncEngine(BaseEngine):
|
|
|
130
155
|
return transition
|
|
131
156
|
return None
|
|
132
157
|
|
|
133
|
-
async def _select_transitions(
|
|
158
|
+
async def _select_transitions(
|
|
134
159
|
self, trigger_data: TriggerData, predicate: Callable
|
|
135
160
|
) -> "OrderedSet[Transition]":
|
|
136
161
|
enabled_transitions: "OrderedSet[Transition]" = OrderedSet()
|
|
@@ -147,7 +172,7 @@ class AsyncEngine(BaseEngine):
|
|
|
147
172
|
async def select_eventless_transitions(self, trigger_data: TriggerData):
|
|
148
173
|
return await self._select_transitions(trigger_data, lambda t, _e: t.is_eventless)
|
|
149
174
|
|
|
150
|
-
async def select_transitions(self, trigger_data: TriggerData) -> "OrderedSet[Transition]":
|
|
175
|
+
async def select_transitions(self, trigger_data: TriggerData) -> "OrderedSet[Transition]":
|
|
151
176
|
return await self._select_transitions(trigger_data, lambda t, e: t.match(e))
|
|
152
177
|
|
|
153
178
|
async def _execute_transition_content(
|
|
@@ -172,7 +197,7 @@ class AsyncEngine(BaseEngine):
|
|
|
172
197
|
|
|
173
198
|
return result
|
|
174
199
|
|
|
175
|
-
async def _exit_states(
|
|
200
|
+
async def _exit_states(
|
|
176
201
|
self, enabled_transitions: "list[Transition]", trigger_data: TriggerData
|
|
177
202
|
) -> "OrderedSet[State]":
|
|
178
203
|
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
@@ -180,24 +205,22 @@ class AsyncEngine(BaseEngine):
|
|
|
180
205
|
|
|
181
206
|
for info in ordered_states:
|
|
182
207
|
# Cancel invocations for this state before executing exit handlers.
|
|
183
|
-
|
|
184
|
-
self._invoke_manager.cancel_for_state(info.state)
|
|
208
|
+
self._invoke_manager.cancel_for_state(info.state)
|
|
185
209
|
|
|
186
210
|
args, kwargs = await self._get_args_kwargs(
|
|
187
211
|
info.transition, trigger_data, source=info.state
|
|
188
212
|
)
|
|
189
213
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
)
|
|
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
|
+
)
|
|
195
218
|
|
|
196
219
|
self._remove_state_from_configuration(info.state)
|
|
197
220
|
|
|
198
221
|
return result
|
|
199
222
|
|
|
200
|
-
async def _enter_states(
|
|
223
|
+
async def _enter_states(
|
|
201
224
|
self,
|
|
202
225
|
enabled_transitions: "list[Transition]",
|
|
203
226
|
trigger_data: TriggerData,
|
|
@@ -329,19 +352,6 @@ class AsyncEngine(BaseEngine):
|
|
|
329
352
|
|
|
330
353
|
# --- Engine loop ---
|
|
331
354
|
|
|
332
|
-
async def _run_microstep(self, enabled_transitions, trigger_data): # pragma: no cover
|
|
333
|
-
"""Run a microstep for internal/eventless transitions with error handling.
|
|
334
|
-
|
|
335
|
-
Note: microstep() handles its own errors internally, so this try/except
|
|
336
|
-
is a safety net that is not expected to be reached in normal operation.
|
|
337
|
-
"""
|
|
338
|
-
try:
|
|
339
|
-
await self.microstep(list(enabled_transitions), trigger_data)
|
|
340
|
-
except InvalidDefinition:
|
|
341
|
-
raise
|
|
342
|
-
except Exception as e:
|
|
343
|
-
self._handle_error(e, trigger_data)
|
|
344
|
-
|
|
345
355
|
async def activate_initial_state(self, **kwargs):
|
|
346
356
|
"""Activate the initial state.
|
|
347
357
|
|
|
@@ -354,162 +364,184 @@ class AsyncEngine(BaseEngine):
|
|
|
354
364
|
"""
|
|
355
365
|
return await self.processing_loop()
|
|
356
366
|
|
|
357
|
-
async def processing_loop(
|
|
358
|
-
|
|
359
|
-
):
|
|
360
|
-
"""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.
|
|
361
369
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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.
|
|
365
373
|
|
|
366
374
|
When ``caller_future`` is provided, the caller can ``await`` it to
|
|
367
375
|
receive its own event's result — even if another coroutine holds the
|
|
368
|
-
processing lock.
|
|
376
|
+
processing lock. If that coroutine is cancelled, the caller takes the loop over.
|
|
369
377
|
"""
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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)
|
|
376
410
|
|
|
377
|
-
|
|
411
|
+
async def _run_processing_loop(self, caller_future: "asyncio.Future[object] | None"):
|
|
378
412
|
self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
|
|
379
413
|
first_result = self._sentinel
|
|
414
|
+
macrostep = _MacrostepCaller()
|
|
380
415
|
try:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
self.
|
|
384
|
-
took_events = False
|
|
385
|
-
macrostep_done = False
|
|
386
|
-
|
|
387
|
-
# Phase 1: eventless transitions and internal events
|
|
388
|
-
while not macrostep_done:
|
|
389
|
-
self._microstep_count = 0
|
|
390
|
-
self._debug(
|
|
391
|
-
"%s Macrostep %d: eventless/internal queue",
|
|
392
|
-
self._log_id,
|
|
393
|
-
self._macrostep_count,
|
|
394
|
-
)
|
|
395
|
-
|
|
416
|
+
with _processing_step(self):
|
|
417
|
+
took_events = True
|
|
418
|
+
while took_events and self.running:
|
|
396
419
|
self.clear_cache()
|
|
397
|
-
|
|
398
|
-
enabled_transitions = await self.select_eventless_transitions(internal_event)
|
|
399
|
-
if not enabled_transitions:
|
|
400
|
-
if self.internal_queue.is_empty():
|
|
401
|
-
macrostep_done = True
|
|
402
|
-
else:
|
|
403
|
-
internal_event = self.internal_queue.pop()
|
|
404
|
-
enabled_transitions = await self.select_transitions(internal_event)
|
|
405
|
-
if enabled_transitions:
|
|
406
|
-
self._debug(
|
|
407
|
-
"%s Enabled transitions: %s", self._log_id, enabled_transitions
|
|
408
|
-
)
|
|
409
|
-
took_events = True
|
|
410
|
-
await self._run_microstep(enabled_transitions, internal_event)
|
|
411
|
-
|
|
412
|
-
# Spawn invoke handlers for states entered during this macrostep.
|
|
413
|
-
await self._invoke_manager.spawn_pending_async()
|
|
414
|
-
self._check_root_final_state()
|
|
415
|
-
|
|
416
|
-
# Phase 2: remaining internal events
|
|
417
|
-
while not self.internal_queue.is_empty(): # pragma: no cover
|
|
418
|
-
internal_event = self.internal_queue.pop()
|
|
419
|
-
enabled_transitions = await self.select_transitions(internal_event)
|
|
420
|
-
if enabled_transitions:
|
|
421
|
-
await self._run_microstep(enabled_transitions, internal_event)
|
|
422
|
-
|
|
423
|
-
# Phase 3: external events
|
|
424
|
-
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
|
|
425
|
-
while not self.external_queue.is_empty():
|
|
426
|
-
self.clear_cache()
|
|
427
|
-
took_events = True
|
|
428
|
-
external_event = self.external_queue.pop()
|
|
429
|
-
current_time = time()
|
|
430
|
-
if external_event.execution_time > current_time:
|
|
431
|
-
self.put(external_event, _delayed=True)
|
|
432
|
-
await asyncio.sleep(self.sm._loop_sleep_in_ms)
|
|
433
|
-
# Break to Phase 1 so internal events and eventless
|
|
434
|
-
# transitions can be processed while we wait.
|
|
435
|
-
break
|
|
436
|
-
|
|
437
|
-
self._macrostep_count += 1
|
|
438
|
-
self._microstep_count = 0
|
|
439
|
-
self._debug(
|
|
440
|
-
"%s macrostep %d: event=%s",
|
|
441
|
-
self._log_id,
|
|
442
|
-
self._macrostep_count,
|
|
443
|
-
external_event.event,
|
|
444
|
-
)
|
|
420
|
+
took_events = await self._run_macrostep()
|
|
445
421
|
|
|
446
|
-
#
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if
|
|
451
|
-
transitions = self._initial_transitions(external_event)
|
|
452
|
-
await self._enter_states(
|
|
453
|
-
transitions, external_event, OrderedSet(), OrderedSet()
|
|
454
|
-
)
|
|
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:
|
|
455
427
|
break
|
|
456
428
|
|
|
457
|
-
|
|
458
|
-
|
|
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
|
|
459
433
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
self._debug(
|
|
464
|
-
"%s Enabled transitions: %s", self._log_id, enabled_transitions
|
|
465
|
-
)
|
|
466
|
-
if enabled_transitions:
|
|
467
|
-
result = await self.microstep(
|
|
468
|
-
list(enabled_transitions), external_event
|
|
469
|
-
)
|
|
470
|
-
self._resolve_future(event_future, result)
|
|
471
|
-
if first_result is self._sentinel:
|
|
472
|
-
first_result = result
|
|
473
|
-
else:
|
|
474
|
-
if not self.sm.allow_event_without_transition:
|
|
475
|
-
tna = TransitionNotAllowed(
|
|
476
|
-
external_event.event, self.sm.configuration
|
|
477
|
-
)
|
|
478
|
-
self._reject_future(event_future, tna)
|
|
479
|
-
self._reject_pending_futures(tna)
|
|
480
|
-
raise tna
|
|
481
|
-
# Event allowed but no transition — resolve with None
|
|
482
|
-
self._resolve_future(event_future, None)
|
|
483
|
-
except Exception as exc:
|
|
484
|
-
self._reject_future(event_future, exc)
|
|
485
|
-
self._reject_pending_futures(exc)
|
|
486
|
-
self.clear()
|
|
487
|
-
raise
|
|
434
|
+
macrostep.answer()
|
|
435
|
+
if not self.running:
|
|
436
|
+
self._drop_external_events()
|
|
488
437
|
|
|
489
438
|
except Exception as exc:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
# get their successful result via ``await future`` below, and
|
|
495
|
-
# the failing event's exception was already routed to *its*
|
|
496
|
-
# caller's future by ``_reject_future(event_future, ...)``.
|
|
497
|
-
self._reject_future(caller_future, exc)
|
|
498
|
-
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:
|
|
499
443
|
raise
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
444
|
+
except BaseException:
|
|
445
|
+
self.internal_queue.clear()
|
|
446
|
+
macrostep.interrupt()
|
|
447
|
+
if caller_future is not None:
|
|
448
|
+
caller_future.cancel()
|
|
449
|
+
raise
|
|
503
450
|
|
|
504
451
|
self._debug("%s Processing loop ended", self._log_id)
|
|
505
452
|
result = first_result if first_result is not self._sentinel else None
|
|
506
|
-
# If the caller has a future, await it (already resolved by now).
|
|
507
453
|
if caller_future is not None:
|
|
508
454
|
# Resolve the future if it wasn't processed (e.g. machine terminated).
|
|
509
455
|
self._resolve_future(caller_future, result)
|
|
510
456
|
return await caller_future
|
|
511
457
|
return result
|
|
512
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
|
+
|
|
513
545
|
async def enabled_events(self, *args, **kwargs):
|
|
514
546
|
sm = self.sm
|
|
515
547
|
enabled = {}
|