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/base.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from collections.abc import Callable
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from contextvars import ContextVar
|
|
3
6
|
from dataclasses import dataclass
|
|
4
7
|
from dataclasses import field
|
|
5
|
-
from
|
|
8
|
+
from heapq import heappop
|
|
6
9
|
from queue import PriorityQueue
|
|
7
10
|
from queue import Queue
|
|
8
11
|
from threading import Lock
|
|
@@ -26,6 +29,24 @@ if TYPE_CHECKING:
|
|
|
26
29
|
|
|
27
30
|
logger = logging.getLogger(__name__)
|
|
28
31
|
|
|
32
|
+
_running_engine: "ContextVar[BaseEngine | None]" = ContextVar("_running_engine", default=None)
|
|
33
|
+
"""The engine whose step the current thread or task is running, if any.
|
|
34
|
+
|
|
35
|
+
A context variable is per thread and per asyncio task: asyncio copies it into the tasks a
|
|
36
|
+
step creates, so callbacks see it, while another thread or an independent coroutine sending
|
|
37
|
+
to the same machine does not.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@contextmanager
|
|
42
|
+
def _processing_step(engine: "BaseEngine") -> Iterator[None]:
|
|
43
|
+
"""Mark the current context as running a step of ``engine``, until the block ends."""
|
|
44
|
+
token = _running_engine.set(engine)
|
|
45
|
+
try:
|
|
46
|
+
yield
|
|
47
|
+
finally:
|
|
48
|
+
_running_engine.reset(token)
|
|
49
|
+
|
|
29
50
|
|
|
30
51
|
@dataclass(frozen=True, unsafe_hash=True, eq=True)
|
|
31
52
|
class StateTransition:
|
|
@@ -33,6 +54,13 @@ class StateTransition:
|
|
|
33
54
|
state: State
|
|
34
55
|
|
|
35
56
|
|
|
57
|
+
#: What entering states needs, worked out before any callback runs: the states in entry order,
|
|
58
|
+
#: those entered by default, the default history content, and the new configuration.
|
|
59
|
+
_EntryPlan = tuple[
|
|
60
|
+
list[StateTransition], OrderedSet[StateTransition], dict[str, Any], OrderedSet[State]
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
36
64
|
class EventQueue:
|
|
37
65
|
def __init__(self):
|
|
38
66
|
self.queue: Queue = PriorityQueue()
|
|
@@ -41,7 +69,16 @@ class EventQueue:
|
|
|
41
69
|
return f"EventQueue({self.queue.queue!r}, size={self.queue.qsize()})"
|
|
42
70
|
|
|
43
71
|
def is_empty(self):
|
|
44
|
-
|
|
72
|
+
"""Whether the queue holds no trigger that has not been cancelled."""
|
|
73
|
+
with self.queue.mutex:
|
|
74
|
+
self._drop_cancelled_head()
|
|
75
|
+
return not self.queue.queue
|
|
76
|
+
|
|
77
|
+
def _drop_cancelled_head(self):
|
|
78
|
+
"""Drop the cancelled triggers at the head. The caller holds the queue's mutex."""
|
|
79
|
+
heap = self.queue.queue
|
|
80
|
+
while heap and heap[0].cancelled:
|
|
81
|
+
heappop(heap)
|
|
45
82
|
|
|
46
83
|
def put(self, trigger_data: TriggerData):
|
|
47
84
|
"""Put the trigger on the queue without blocking the caller."""
|
|
@@ -51,10 +88,30 @@ class EventQueue:
|
|
|
51
88
|
"""Pop a trigger from the queue without blocking the caller."""
|
|
52
89
|
return self.queue.get(block=False)
|
|
53
90
|
|
|
91
|
+
def pop_due(self, now: float) -> "TriggerData | None":
|
|
92
|
+
"""Pop the trigger at the head if it is due by ``now``.
|
|
93
|
+
|
|
94
|
+
Returns ``None`` when the queue is empty or its head is not yet due. The head stays in
|
|
95
|
+
the queue until it is popped, so a :meth:`cancel` from another thread always finds it.
|
|
96
|
+
"""
|
|
97
|
+
with self.queue.mutex:
|
|
98
|
+
self._drop_cancelled_head()
|
|
99
|
+
heap: list[TriggerData] = self.queue.queue
|
|
100
|
+
if not heap or heap[0].execution_time > now:
|
|
101
|
+
return None
|
|
102
|
+
return heappop(heap)
|
|
103
|
+
|
|
54
104
|
def clear(self):
|
|
55
105
|
with self.queue.mutex:
|
|
56
106
|
self.queue.queue.clear()
|
|
57
107
|
|
|
108
|
+
def drain(self) -> list[TriggerData]:
|
|
109
|
+
"""Remove every trigger from the queue and return those not cancelled."""
|
|
110
|
+
with self.queue.mutex:
|
|
111
|
+
triggers = [trigger for trigger in self.queue.queue if not trigger.cancelled]
|
|
112
|
+
self.queue.queue.clear()
|
|
113
|
+
return triggers
|
|
114
|
+
|
|
58
115
|
def reject_futures(self, exc: Exception):
|
|
59
116
|
"""Reject all unresolved futures in the queue.
|
|
60
117
|
|
|
@@ -67,15 +124,17 @@ class EventQueue:
|
|
|
67
124
|
if future is not None and not future.done():
|
|
68
125
|
future.set_exception(exc)
|
|
69
126
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
def cancel(self, send_id: str):
|
|
128
|
+
"""Cancel every trigger with this ``send_id``, and answer a caller awaiting one with
|
|
129
|
+
``None``. Each stays where it is, so the heap keeps its order, until :meth:`is_empty`
|
|
130
|
+
drops it from the head."""
|
|
73
131
|
with self.queue.mutex:
|
|
74
|
-
self.queue.queue
|
|
75
|
-
trigger_data
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
132
|
+
for trigger_data in self.queue.queue:
|
|
133
|
+
if trigger_data.send_id == send_id:
|
|
134
|
+
trigger_data.cancelled = True
|
|
135
|
+
future = trigger_data.future
|
|
136
|
+
if future is not None and not future.done():
|
|
137
|
+
future.set_result(None)
|
|
79
138
|
|
|
80
139
|
|
|
81
140
|
_ERROR_EXECUTION = "error.execution"
|
|
@@ -97,40 +156,55 @@ class BaseEngine:
|
|
|
97
156
|
self._debug = logger.debug if logger.isEnabledFor(logging.DEBUG) else lambda *a, **k: None
|
|
98
157
|
self._root_parallel_final_pending: "State | None" = None
|
|
99
158
|
|
|
100
|
-
def empty(self): # pragma: no cover
|
|
101
|
-
return self.external_queue.is_empty()
|
|
102
|
-
|
|
103
159
|
def clear_cache(self):
|
|
104
160
|
"""Clears the cache. Should be called at the start of each processing loop."""
|
|
105
161
|
self._cache.clear()
|
|
106
162
|
|
|
107
|
-
def put(self, trigger_data: TriggerData, internal: bool = False
|
|
108
|
-
"""Put the trigger on the queue without blocking the caller.
|
|
163
|
+
def put(self, trigger_data: TriggerData, internal: bool = False):
|
|
164
|
+
"""Put the trigger on the queue without blocking the caller.
|
|
165
|
+
|
|
166
|
+
An ``internal`` trigger joins the internal queue only when this machine raises it while
|
|
167
|
+
running a step. Put from outside, including from a step of another machine, there is
|
|
168
|
+
no macrostep to join, so it goes to the external queue, where its caller gets its
|
|
169
|
+
result.
|
|
170
|
+
"""
|
|
109
171
|
if not self.running and not self.sm.allow_event_without_transition:
|
|
110
172
|
raise TransitionNotAllowed(trigger_data.event, self.sm.configuration)
|
|
111
173
|
|
|
174
|
+
internal = internal and _running_engine.get() is self
|
|
112
175
|
if internal:
|
|
113
176
|
self.internal_queue.put(trigger_data)
|
|
114
177
|
else:
|
|
115
178
|
self.external_queue.put(trigger_data)
|
|
116
179
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
def pop(self): # pragma: no cover
|
|
126
|
-
return self.external_queue.pop()
|
|
180
|
+
self._debug(
|
|
181
|
+
"%s New event '%s' put on the '%s' queue",
|
|
182
|
+
self._log_id,
|
|
183
|
+
trigger_data.event,
|
|
184
|
+
"internal" if internal else "external",
|
|
185
|
+
)
|
|
127
186
|
|
|
128
187
|
def clear(self):
|
|
188
|
+
self.internal_queue.clear()
|
|
129
189
|
self.external_queue.clear()
|
|
130
190
|
|
|
191
|
+
def _drop_external_events(self):
|
|
192
|
+
"""Drop the external events left once the machine has stopped, answering a caller
|
|
193
|
+
awaiting one as the stopped machine answers a new event."""
|
|
194
|
+
for trigger_data in self.external_queue.drain():
|
|
195
|
+
future = trigger_data.future
|
|
196
|
+
if future is None or future.done():
|
|
197
|
+
continue
|
|
198
|
+
if self.sm.allow_event_without_transition:
|
|
199
|
+
future.set_result(None)
|
|
200
|
+
else:
|
|
201
|
+
future.set_exception(
|
|
202
|
+
TransitionNotAllowed(trigger_data.event, self.sm.configuration)
|
|
203
|
+
)
|
|
204
|
+
|
|
131
205
|
def cancel_event(self, send_id: str):
|
|
132
206
|
"""Cancel the event with the given send_id."""
|
|
133
|
-
self.external_queue.
|
|
207
|
+
self.external_queue.cancel(send_id)
|
|
134
208
|
|
|
135
209
|
def _on_error_handler(self) -> "Callable[[Exception], None] | None":
|
|
136
210
|
"""Return a per-block error handler, or ``None``.
|
|
@@ -324,114 +398,31 @@ class BaseEngine:
|
|
|
324
398
|
|
|
325
399
|
return targets
|
|
326
400
|
|
|
327
|
-
def
|
|
328
|
-
"""
|
|
329
|
-
Select the eventless transitions that match the trigger data.
|
|
330
|
-
"""
|
|
331
|
-
return self._select_transitions(trigger_data, lambda t, _e: t.is_eventless)
|
|
332
|
-
|
|
333
|
-
def select_transitions(self, trigger_data: TriggerData) -> OrderedSet[Transition]:
|
|
334
|
-
"""
|
|
335
|
-
Select the transitions that match the trigger data.
|
|
336
|
-
"""
|
|
337
|
-
return self._select_transitions(trigger_data, lambda t, e: t.match(e))
|
|
338
|
-
|
|
339
|
-
def _first_transition_that_matches(
|
|
401
|
+
def _get_args_kwargs(
|
|
340
402
|
self,
|
|
341
|
-
|
|
403
|
+
transition: Transition,
|
|
342
404
|
trigger_data: TriggerData,
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
for s in chain([state], state.ancestors()):
|
|
346
|
-
transition: Transition
|
|
347
|
-
for transition in s.transitions:
|
|
348
|
-
if (
|
|
349
|
-
not transition.initial
|
|
350
|
-
and predicate(transition, trigger_data.event)
|
|
351
|
-
and self._conditions_match(transition, trigger_data)
|
|
352
|
-
):
|
|
353
|
-
return transition
|
|
354
|
-
return None
|
|
355
|
-
|
|
356
|
-
def _select_transitions(
|
|
357
|
-
self, trigger_data: TriggerData, predicate: Callable
|
|
358
|
-
) -> OrderedSet[Transition]:
|
|
359
|
-
"""Select the transitions that match the trigger data."""
|
|
360
|
-
enabled_transitions = OrderedSet[Transition]()
|
|
361
|
-
|
|
362
|
-
# Get atomic states, TODO: sorted by document order
|
|
363
|
-
atomic_states = (state for state in self.sm.configuration if state.is_atomic)
|
|
364
|
-
|
|
365
|
-
for state in atomic_states:
|
|
366
|
-
transition = self._first_transition_that_matches(state, trigger_data, predicate)
|
|
367
|
-
if transition is not None:
|
|
368
|
-
enabled_transitions.add(transition)
|
|
369
|
-
|
|
370
|
-
return self._filter_conflicting_transitions(enabled_transitions)
|
|
371
|
-
|
|
372
|
-
def microstep(self, transitions: list[Transition], trigger_data: TriggerData):
|
|
373
|
-
"""Process a single set of transitions in a 'lock step'.
|
|
374
|
-
This includes exiting states, executing transition content, and entering states.
|
|
375
|
-
"""
|
|
376
|
-
self._microstep_count += 1
|
|
377
|
-
self._debug(
|
|
378
|
-
"%s macro:%d micro:%d transitions: %s",
|
|
379
|
-
self._log_id,
|
|
380
|
-
self._macrostep_count,
|
|
381
|
-
self._microstep_count,
|
|
382
|
-
transitions,
|
|
383
|
-
)
|
|
384
|
-
previous_configuration = self.sm.configuration
|
|
385
|
-
try:
|
|
386
|
-
result = self._execute_transition_content(
|
|
387
|
-
transitions, trigger_data, lambda t: t.before.key
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
states_to_exit = self._exit_states(transitions, trigger_data)
|
|
391
|
-
result += self._enter_states(
|
|
392
|
-
transitions, trigger_data, states_to_exit, previous_configuration
|
|
393
|
-
)
|
|
394
|
-
except InvalidDefinition:
|
|
395
|
-
self.sm.configuration = previous_configuration
|
|
396
|
-
raise
|
|
397
|
-
except Exception as e:
|
|
398
|
-
self.sm.configuration = previous_configuration
|
|
399
|
-
self._handle_error(e, trigger_data)
|
|
400
|
-
return None
|
|
401
|
-
|
|
402
|
-
try:
|
|
403
|
-
self._execute_transition_content(
|
|
404
|
-
transitions,
|
|
405
|
-
trigger_data,
|
|
406
|
-
lambda t: t.after.key,
|
|
407
|
-
set_target_as_state=True,
|
|
408
|
-
)
|
|
409
|
-
except InvalidDefinition:
|
|
410
|
-
raise
|
|
411
|
-
except Exception as e:
|
|
412
|
-
self._handle_error(e, trigger_data)
|
|
413
|
-
|
|
414
|
-
if len(result) == 0:
|
|
415
|
-
result = None
|
|
416
|
-
elif len(result) == 1:
|
|
417
|
-
result = result[0]
|
|
418
|
-
|
|
419
|
-
return result
|
|
420
|
-
|
|
421
|
-
def _get_args_kwargs(
|
|
422
|
-
self, transition: Transition, trigger_data: TriggerData, target: "State | None" = None
|
|
405
|
+
target: "State | None" = None,
|
|
406
|
+
source: "State | None" = None,
|
|
423
407
|
):
|
|
424
408
|
# Generate a unique key for the cache, the cache is invalidated once per loop
|
|
425
|
-
cache_key = (id(transition), id(trigger_data), id(target))
|
|
409
|
+
cache_key = (id(transition), id(trigger_data), id(target), id(source))
|
|
426
410
|
|
|
427
411
|
# Check the cache for existing results
|
|
428
412
|
if cache_key in self._cache:
|
|
429
413
|
return self._cache[cache_key]
|
|
430
414
|
|
|
431
415
|
event_data = EventData(trigger_data=trigger_data, transition=transition)
|
|
416
|
+
# Bind `state`/`target` to the individual state being entered, and
|
|
417
|
+
# `state`/`source` to the individual state being exited, so the generic
|
|
418
|
+
# `on_enter_state`/`on_exit_state` callbacks stay symmetric when crossing
|
|
419
|
+
# compound state boundaries (the transition only knows its own endpoints).
|
|
432
420
|
if target:
|
|
433
421
|
event_data.state = target
|
|
434
422
|
event_data.target = target
|
|
423
|
+
if source:
|
|
424
|
+
event_data.state = source
|
|
425
|
+
event_data.source = source
|
|
435
426
|
|
|
436
427
|
args, kwargs = event_data.args, event_data.extended_kwargs
|
|
437
428
|
|
|
@@ -468,7 +459,7 @@ class BaseEngine:
|
|
|
468
459
|
state = info.state
|
|
469
460
|
for history in state.history:
|
|
470
461
|
if history.type.is_deep:
|
|
471
|
-
history_value = [s for s in self.sm.configuration if s.is_descendant(state)]
|
|
462
|
+
history_value = [s for s in self.sm.configuration if s.is_descendant(state)]
|
|
472
463
|
else: # shallow history
|
|
473
464
|
history_value = [s for s in self.sm.configuration if s.parent == state]
|
|
474
465
|
|
|
@@ -488,29 +479,6 @@ class BaseEngine:
|
|
|
488
479
|
if not self.sm.atomic_configuration_update:
|
|
489
480
|
self.sm._config.discard(state)
|
|
490
481
|
|
|
491
|
-
def _exit_states(
|
|
492
|
-
self, enabled_transitions: list[Transition], trigger_data: TriggerData
|
|
493
|
-
) -> OrderedSet[State]:
|
|
494
|
-
"""Compute and process the states to exit for the given transitions."""
|
|
495
|
-
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
496
|
-
on_error = self._on_error_handler()
|
|
497
|
-
|
|
498
|
-
for info in ordered_states:
|
|
499
|
-
# Cancel invocations for this state before executing exit handlers.
|
|
500
|
-
if info.state is not None: # pragma: no branch
|
|
501
|
-
self._invoke_manager.cancel_for_state(info.state)
|
|
502
|
-
|
|
503
|
-
args, kwargs = self._get_args_kwargs(info.transition, trigger_data)
|
|
504
|
-
|
|
505
|
-
# Execute `onexit` handlers — same per-block error isolation as onentry.
|
|
506
|
-
if info.state is not None: # pragma: no branch
|
|
507
|
-
self._debug("%s Exiting state: %s", self._log_id, info.state)
|
|
508
|
-
self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs)
|
|
509
|
-
|
|
510
|
-
self._remove_state_from_configuration(info.state)
|
|
511
|
-
|
|
512
|
-
return result
|
|
513
|
-
|
|
514
482
|
def _execute_transition_content(
|
|
515
483
|
self,
|
|
516
484
|
enabled_transitions: list[Transition],
|
|
@@ -538,7 +506,7 @@ class BaseEngine:
|
|
|
538
506
|
enabled_transitions: list[Transition],
|
|
539
507
|
states_to_exit: OrderedSet[State],
|
|
540
508
|
previous_configuration: OrderedSet[State],
|
|
541
|
-
) ->
|
|
509
|
+
) -> _EntryPlan:
|
|
542
510
|
"""Compute entry set, ordering, and new configuration. Pure computation, no callbacks.
|
|
543
511
|
|
|
544
512
|
Returns:
|
|
@@ -579,7 +547,7 @@ class BaseEngine:
|
|
|
579
547
|
self.running = False
|
|
580
548
|
try:
|
|
581
549
|
self._invoke_manager.cancel_all()
|
|
582
|
-
except Exception:
|
|
550
|
+
except Exception:
|
|
583
551
|
self._debug("%s Error stopping engine", self._log_id, exc_info=True)
|
|
584
552
|
|
|
585
553
|
def __del__(self):
|
|
@@ -622,7 +590,7 @@ class BaseEngine:
|
|
|
622
590
|
if grandparent.parent is None:
|
|
623
591
|
self._root_parallel_final_pending = grandparent
|
|
624
592
|
|
|
625
|
-
def _enter_states(
|
|
593
|
+
def _enter_states(
|
|
626
594
|
self,
|
|
627
595
|
enabled_transitions: list[Transition],
|
|
628
596
|
trigger_data: TriggerData,
|
|
@@ -744,7 +712,7 @@ class BaseEngine:
|
|
|
744
712
|
default_history_content,
|
|
745
713
|
)
|
|
746
714
|
|
|
747
|
-
def add_descendant_states_to_enter(
|
|
715
|
+
def add_descendant_states_to_enter(
|
|
748
716
|
self,
|
|
749
717
|
info: StateTransition,
|
|
750
718
|
states_to_enter,
|
|
@@ -764,121 +732,164 @@ class BaseEngine:
|
|
|
764
732
|
state = info.state
|
|
765
733
|
|
|
766
734
|
if state and state.is_history:
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
self._log_id,
|
|
775
|
-
state.parent,
|
|
776
|
-
state,
|
|
777
|
-
state.type.value,
|
|
778
|
-
[s.id for s in self.sm.history_values[state.id]],
|
|
779
|
-
)
|
|
780
|
-
for history_state in self.sm.history_values[state.id]:
|
|
781
|
-
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
782
|
-
if state.type.is_deep:
|
|
783
|
-
states_to_enter.add(info_to_add)
|
|
784
|
-
else:
|
|
785
|
-
self.add_descendant_states_to_enter(
|
|
786
|
-
info_to_add,
|
|
787
|
-
states_to_enter,
|
|
788
|
-
states_for_default_entry,
|
|
789
|
-
default_history_content,
|
|
790
|
-
)
|
|
791
|
-
for history_state in self.sm.history_values[state.id]:
|
|
792
|
-
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
793
|
-
self.add_ancestor_states_to_enter(
|
|
794
|
-
info_to_add,
|
|
795
|
-
state.parent,
|
|
796
|
-
states_to_enter,
|
|
797
|
-
states_for_default_entry,
|
|
798
|
-
default_history_content,
|
|
799
|
-
)
|
|
800
|
-
else:
|
|
801
|
-
# Handle default history content
|
|
802
|
-
self._debug(
|
|
803
|
-
"%s History state '%s.%s' default content: %s",
|
|
804
|
-
self._log_id,
|
|
805
|
-
state.parent,
|
|
806
|
-
state,
|
|
807
|
-
[t.target.id for t in state.transitions if t.target],
|
|
808
|
-
)
|
|
809
|
-
|
|
810
|
-
for transition in state.transitions:
|
|
811
|
-
target = cast(State, transition.target)
|
|
812
|
-
info_history = StateTransition(transition=transition, state=target)
|
|
813
|
-
default_history_content[parent_id].append(info_history)
|
|
814
|
-
self.add_descendant_states_to_enter(
|
|
815
|
-
info_history,
|
|
816
|
-
states_to_enter,
|
|
817
|
-
states_for_default_entry,
|
|
818
|
-
default_history_content,
|
|
819
|
-
) # noqa: E501
|
|
820
|
-
for transition in state.transitions:
|
|
821
|
-
target = cast(State, transition.target)
|
|
822
|
-
info_history = StateTransition(transition=transition, state=target)
|
|
823
|
-
|
|
824
|
-
self.add_ancestor_states_to_enter(
|
|
825
|
-
info_history,
|
|
826
|
-
state.parent,
|
|
827
|
-
states_to_enter,
|
|
828
|
-
states_for_default_entry,
|
|
829
|
-
default_history_content,
|
|
830
|
-
) # noqa: E501
|
|
735
|
+
self._add_history_states_to_enter(
|
|
736
|
+
info,
|
|
737
|
+
cast(HistoryState, state),
|
|
738
|
+
states_to_enter,
|
|
739
|
+
states_for_default_entry,
|
|
740
|
+
default_history_content,
|
|
741
|
+
)
|
|
831
742
|
return
|
|
832
743
|
|
|
833
744
|
# Add the state to the entry set
|
|
834
|
-
if (
|
|
835
|
-
self.sm.enable_self_transition_entries
|
|
836
|
-
or not info.transition.internal
|
|
837
|
-
or not (
|
|
838
|
-
info.transition.is_self
|
|
839
|
-
or (
|
|
840
|
-
info.transition.target
|
|
841
|
-
and info.transition.target.is_descendant(info.transition.source)
|
|
842
|
-
)
|
|
843
|
-
)
|
|
844
|
-
):
|
|
745
|
+
if self._enters_target(info.transition):
|
|
845
746
|
states_to_enter.add(info)
|
|
846
747
|
state = info.state
|
|
847
748
|
|
|
848
749
|
if state.parallel:
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
750
|
+
self._add_regions_to_enter(
|
|
751
|
+
info, states_to_enter, states_for_default_entry, default_history_content
|
|
752
|
+
)
|
|
753
|
+
elif state.is_compound:
|
|
754
|
+
self._add_initial_states_to_enter(
|
|
755
|
+
info, states_to_enter, states_for_default_entry, default_history_content
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
def _enters_target(self, transition: Transition) -> bool:
|
|
759
|
+
"""Whether taking ``transition`` enters its target.
|
|
760
|
+
|
|
761
|
+
An internal transition to its own source, or to a descendant of it, does not, unless
|
|
762
|
+
the machine enables self-transition entries.
|
|
763
|
+
"""
|
|
764
|
+
return (
|
|
765
|
+
self.sm.enable_self_transition_entries
|
|
766
|
+
or not transition.internal
|
|
767
|
+
or not (
|
|
768
|
+
transition.is_self
|
|
769
|
+
or (transition.target and transition.target.is_descendant(transition.source))
|
|
770
|
+
)
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
def _add_history_states_to_enter(
|
|
774
|
+
self,
|
|
775
|
+
info: StateTransition,
|
|
776
|
+
state: HistoryState,
|
|
777
|
+
states_to_enter,
|
|
778
|
+
states_for_default_entry,
|
|
779
|
+
default_history_content,
|
|
780
|
+
):
|
|
781
|
+
"""Add the states a history state stands for: those it recorded, or its default."""
|
|
782
|
+
parent_id = state.parent and state.parent.id
|
|
783
|
+
default_history_content[parent_id] = [info]
|
|
784
|
+
if state.id in self.sm.history_values:
|
|
785
|
+
self._debug(
|
|
786
|
+
"%s History state '%s.%s' %s restoring: '%s'",
|
|
787
|
+
self._log_id,
|
|
788
|
+
state.parent,
|
|
789
|
+
state,
|
|
790
|
+
state.type.value,
|
|
791
|
+
[s.id for s in self.sm.history_values[state.id]],
|
|
792
|
+
)
|
|
793
|
+
for history_state in self.sm.history_values[state.id]:
|
|
794
|
+
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
795
|
+
if state.type.is_deep:
|
|
796
|
+
states_to_enter.add(info_to_add)
|
|
797
|
+
else:
|
|
854
798
|
self.add_descendant_states_to_enter(
|
|
855
799
|
info_to_add,
|
|
856
800
|
states_to_enter,
|
|
857
801
|
states_for_default_entry,
|
|
858
802
|
default_history_content,
|
|
859
803
|
)
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
804
|
+
for history_state in self.sm.history_values[state.id]:
|
|
805
|
+
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
806
|
+
self.add_ancestor_states_to_enter(
|
|
807
|
+
info_to_add,
|
|
808
|
+
state.parent,
|
|
809
|
+
states_to_enter,
|
|
810
|
+
states_for_default_entry,
|
|
811
|
+
default_history_content,
|
|
812
|
+
)
|
|
813
|
+
else:
|
|
814
|
+
# Handle default history content
|
|
815
|
+
self._debug(
|
|
816
|
+
"%s History state '%s.%s' default content: %s",
|
|
817
|
+
self._log_id,
|
|
818
|
+
state.parent,
|
|
819
|
+
state,
|
|
820
|
+
[t.target.id for t in state.transitions if t.target],
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
for transition in state.transitions:
|
|
824
|
+
target = cast(State, transition.target)
|
|
825
|
+
info_history = StateTransition(transition=transition, state=target)
|
|
826
|
+
default_history_content[parent_id].append(info_history)
|
|
866
827
|
self.add_descendant_states_to_enter(
|
|
867
|
-
|
|
828
|
+
info_history,
|
|
868
829
|
states_to_enter,
|
|
869
830
|
states_for_default_entry,
|
|
870
831
|
default_history_content,
|
|
871
832
|
)
|
|
872
|
-
for
|
|
873
|
-
|
|
833
|
+
for transition in state.transitions:
|
|
834
|
+
target = cast(State, transition.target)
|
|
835
|
+
info_history = StateTransition(transition=transition, state=target)
|
|
836
|
+
|
|
874
837
|
self.add_ancestor_states_to_enter(
|
|
875
|
-
|
|
876
|
-
state,
|
|
838
|
+
info_history,
|
|
839
|
+
state.parent,
|
|
877
840
|
states_to_enter,
|
|
878
841
|
states_for_default_entry,
|
|
879
842
|
default_history_content,
|
|
880
843
|
)
|
|
881
844
|
|
|
845
|
+
def _add_regions_to_enter(
|
|
846
|
+
self,
|
|
847
|
+
info: StateTransition,
|
|
848
|
+
states_to_enter,
|
|
849
|
+
states_for_default_entry,
|
|
850
|
+
default_history_content,
|
|
851
|
+
):
|
|
852
|
+
"""Add each region of a parallel state, unless a state inside it is already entered."""
|
|
853
|
+
for child_state in info.state.states:
|
|
854
|
+
if not any(s.state.is_descendant(child_state) for s in states_to_enter):
|
|
855
|
+
info_to_add = StateTransition(transition=info.transition, state=child_state)
|
|
856
|
+
self.add_descendant_states_to_enter(
|
|
857
|
+
info_to_add,
|
|
858
|
+
states_to_enter,
|
|
859
|
+
states_for_default_entry,
|
|
860
|
+
default_history_content,
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
def _add_initial_states_to_enter(
|
|
864
|
+
self,
|
|
865
|
+
info: StateTransition,
|
|
866
|
+
states_to_enter,
|
|
867
|
+
states_for_default_entry,
|
|
868
|
+
default_history_content,
|
|
869
|
+
):
|
|
870
|
+
"""Add the states a compound state's initial transition targets."""
|
|
871
|
+
state = info.state
|
|
872
|
+
states_for_default_entry.add(info)
|
|
873
|
+
transition = next(t for t in state.transitions if t.initial)
|
|
874
|
+
# Process all targets (supports multi-target initial transitions for parallel regions)
|
|
875
|
+
for initial_target in transition.targets:
|
|
876
|
+
info_initial = StateTransition(transition=transition, state=initial_target)
|
|
877
|
+
self.add_descendant_states_to_enter(
|
|
878
|
+
info_initial,
|
|
879
|
+
states_to_enter,
|
|
880
|
+
states_for_default_entry,
|
|
881
|
+
default_history_content,
|
|
882
|
+
)
|
|
883
|
+
for initial_target in transition.targets:
|
|
884
|
+
info_initial = StateTransition(transition=transition, state=initial_target)
|
|
885
|
+
self.add_ancestor_states_to_enter(
|
|
886
|
+
info_initial,
|
|
887
|
+
state,
|
|
888
|
+
states_to_enter,
|
|
889
|
+
states_for_default_entry,
|
|
890
|
+
default_history_content,
|
|
891
|
+
)
|
|
892
|
+
|
|
882
893
|
def add_ancestor_states_to_enter(
|
|
883
894
|
self,
|
|
884
895
|
info: StateTransition,
|
|
@@ -941,9 +952,10 @@ class BaseEngine:
|
|
|
941
952
|
self.running = False
|
|
942
953
|
|
|
943
954
|
def is_in_final_state(self, state: State) -> bool:
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
955
|
+
"""Whether every region of a parallel state, or a compound state, rests in a final child.
|
|
956
|
+
|
|
957
|
+
An atomic state has no children, so it never does.
|
|
958
|
+
"""
|
|
959
|
+
if state.parallel:
|
|
947
960
|
return all(self.is_in_final_state(s) for s in state.states)
|
|
948
|
-
|
|
949
|
-
return False
|
|
961
|
+
return any(s.final and s in self.sm.configuration for s in state.states)
|