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/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,100 +398,6 @@ class BaseEngine:
|
|
|
324
398
|
|
|
325
399
|
return targets
|
|
326
400
|
|
|
327
|
-
def select_eventless_transitions(self, trigger_data: TriggerData):
|
|
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(
|
|
340
|
-
self,
|
|
341
|
-
state: State,
|
|
342
|
-
trigger_data: TriggerData,
|
|
343
|
-
predicate: Callable,
|
|
344
|
-
) -> "Transition | None":
|
|
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
401
|
def _get_args_kwargs(
|
|
422
402
|
self,
|
|
423
403
|
transition: Transition,
|
|
@@ -479,7 +459,7 @@ class BaseEngine:
|
|
|
479
459
|
state = info.state
|
|
480
460
|
for history in state.history:
|
|
481
461
|
if history.type.is_deep:
|
|
482
|
-
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)]
|
|
483
463
|
else: # shallow history
|
|
484
464
|
history_value = [s for s in self.sm.configuration if s.parent == state]
|
|
485
465
|
|
|
@@ -499,29 +479,6 @@ class BaseEngine:
|
|
|
499
479
|
if not self.sm.atomic_configuration_update:
|
|
500
480
|
self.sm._config.discard(state)
|
|
501
481
|
|
|
502
|
-
def _exit_states(
|
|
503
|
-
self, enabled_transitions: list[Transition], trigger_data: TriggerData
|
|
504
|
-
) -> OrderedSet[State]:
|
|
505
|
-
"""Compute and process the states to exit for the given transitions."""
|
|
506
|
-
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
507
|
-
on_error = self._on_error_handler()
|
|
508
|
-
|
|
509
|
-
for info in ordered_states:
|
|
510
|
-
# Cancel invocations for this state before executing exit handlers.
|
|
511
|
-
if info.state is not None: # pragma: no branch
|
|
512
|
-
self._invoke_manager.cancel_for_state(info.state)
|
|
513
|
-
|
|
514
|
-
args, kwargs = self._get_args_kwargs(info.transition, trigger_data, source=info.state)
|
|
515
|
-
|
|
516
|
-
# Execute `onexit` handlers — same per-block error isolation as onentry.
|
|
517
|
-
if info.state is not None: # pragma: no branch
|
|
518
|
-
self._debug("%s Exiting state: %s", self._log_id, info.state)
|
|
519
|
-
self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs)
|
|
520
|
-
|
|
521
|
-
self._remove_state_from_configuration(info.state)
|
|
522
|
-
|
|
523
|
-
return result
|
|
524
|
-
|
|
525
482
|
def _execute_transition_content(
|
|
526
483
|
self,
|
|
527
484
|
enabled_transitions: list[Transition],
|
|
@@ -549,7 +506,7 @@ class BaseEngine:
|
|
|
549
506
|
enabled_transitions: list[Transition],
|
|
550
507
|
states_to_exit: OrderedSet[State],
|
|
551
508
|
previous_configuration: OrderedSet[State],
|
|
552
|
-
) ->
|
|
509
|
+
) -> _EntryPlan:
|
|
553
510
|
"""Compute entry set, ordering, and new configuration. Pure computation, no callbacks.
|
|
554
511
|
|
|
555
512
|
Returns:
|
|
@@ -590,7 +547,7 @@ class BaseEngine:
|
|
|
590
547
|
self.running = False
|
|
591
548
|
try:
|
|
592
549
|
self._invoke_manager.cancel_all()
|
|
593
|
-
except Exception:
|
|
550
|
+
except Exception:
|
|
594
551
|
self._debug("%s Error stopping engine", self._log_id, exc_info=True)
|
|
595
552
|
|
|
596
553
|
def __del__(self):
|
|
@@ -633,7 +590,7 @@ class BaseEngine:
|
|
|
633
590
|
if grandparent.parent is None:
|
|
634
591
|
self._root_parallel_final_pending = grandparent
|
|
635
592
|
|
|
636
|
-
def _enter_states(
|
|
593
|
+
def _enter_states(
|
|
637
594
|
self,
|
|
638
595
|
enabled_transitions: list[Transition],
|
|
639
596
|
trigger_data: TriggerData,
|
|
@@ -755,7 +712,7 @@ class BaseEngine:
|
|
|
755
712
|
default_history_content,
|
|
756
713
|
)
|
|
757
714
|
|
|
758
|
-
def add_descendant_states_to_enter(
|
|
715
|
+
def add_descendant_states_to_enter(
|
|
759
716
|
self,
|
|
760
717
|
info: StateTransition,
|
|
761
718
|
states_to_enter,
|
|
@@ -775,121 +732,164 @@ class BaseEngine:
|
|
|
775
732
|
state = info.state
|
|
776
733
|
|
|
777
734
|
if state and state.is_history:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
self._log_id,
|
|
786
|
-
state.parent,
|
|
787
|
-
state,
|
|
788
|
-
state.type.value,
|
|
789
|
-
[s.id for s in self.sm.history_values[state.id]],
|
|
790
|
-
)
|
|
791
|
-
for history_state in self.sm.history_values[state.id]:
|
|
792
|
-
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
793
|
-
if state.type.is_deep:
|
|
794
|
-
states_to_enter.add(info_to_add)
|
|
795
|
-
else:
|
|
796
|
-
self.add_descendant_states_to_enter(
|
|
797
|
-
info_to_add,
|
|
798
|
-
states_to_enter,
|
|
799
|
-
states_for_default_entry,
|
|
800
|
-
default_history_content,
|
|
801
|
-
)
|
|
802
|
-
for history_state in self.sm.history_values[state.id]:
|
|
803
|
-
info_to_add = StateTransition(transition=info.transition, state=history_state)
|
|
804
|
-
self.add_ancestor_states_to_enter(
|
|
805
|
-
info_to_add,
|
|
806
|
-
state.parent,
|
|
807
|
-
states_to_enter,
|
|
808
|
-
states_for_default_entry,
|
|
809
|
-
default_history_content,
|
|
810
|
-
)
|
|
811
|
-
else:
|
|
812
|
-
# Handle default history content
|
|
813
|
-
self._debug(
|
|
814
|
-
"%s History state '%s.%s' default content: %s",
|
|
815
|
-
self._log_id,
|
|
816
|
-
state.parent,
|
|
817
|
-
state,
|
|
818
|
-
[t.target.id for t in state.transitions if t.target],
|
|
819
|
-
)
|
|
820
|
-
|
|
821
|
-
for transition in state.transitions:
|
|
822
|
-
target = cast(State, transition.target)
|
|
823
|
-
info_history = StateTransition(transition=transition, state=target)
|
|
824
|
-
default_history_content[parent_id].append(info_history)
|
|
825
|
-
self.add_descendant_states_to_enter(
|
|
826
|
-
info_history,
|
|
827
|
-
states_to_enter,
|
|
828
|
-
states_for_default_entry,
|
|
829
|
-
default_history_content,
|
|
830
|
-
) # noqa: E501
|
|
831
|
-
for transition in state.transitions:
|
|
832
|
-
target = cast(State, transition.target)
|
|
833
|
-
info_history = StateTransition(transition=transition, state=target)
|
|
834
|
-
|
|
835
|
-
self.add_ancestor_states_to_enter(
|
|
836
|
-
info_history,
|
|
837
|
-
state.parent,
|
|
838
|
-
states_to_enter,
|
|
839
|
-
states_for_default_entry,
|
|
840
|
-
default_history_content,
|
|
841
|
-
) # 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
|
+
)
|
|
842
742
|
return
|
|
843
743
|
|
|
844
744
|
# Add the state to the entry set
|
|
845
|
-
if (
|
|
846
|
-
self.sm.enable_self_transition_entries
|
|
847
|
-
or not info.transition.internal
|
|
848
|
-
or not (
|
|
849
|
-
info.transition.is_self
|
|
850
|
-
or (
|
|
851
|
-
info.transition.target
|
|
852
|
-
and info.transition.target.is_descendant(info.transition.source)
|
|
853
|
-
)
|
|
854
|
-
)
|
|
855
|
-
):
|
|
745
|
+
if self._enters_target(info.transition):
|
|
856
746
|
states_to_enter.add(info)
|
|
857
747
|
state = info.state
|
|
858
748
|
|
|
859
749
|
if state.parallel:
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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:
|
|
865
798
|
self.add_descendant_states_to_enter(
|
|
866
799
|
info_to_add,
|
|
867
800
|
states_to_enter,
|
|
868
801
|
states_for_default_entry,
|
|
869
802
|
default_history_content,
|
|
870
803
|
)
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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)
|
|
877
827
|
self.add_descendant_states_to_enter(
|
|
878
|
-
|
|
828
|
+
info_history,
|
|
879
829
|
states_to_enter,
|
|
880
830
|
states_for_default_entry,
|
|
881
831
|
default_history_content,
|
|
882
832
|
)
|
|
883
|
-
for
|
|
884
|
-
|
|
833
|
+
for transition in state.transitions:
|
|
834
|
+
target = cast(State, transition.target)
|
|
835
|
+
info_history = StateTransition(transition=transition, state=target)
|
|
836
|
+
|
|
885
837
|
self.add_ancestor_states_to_enter(
|
|
886
|
-
|
|
887
|
-
state,
|
|
838
|
+
info_history,
|
|
839
|
+
state.parent,
|
|
888
840
|
states_to_enter,
|
|
889
841
|
states_for_default_entry,
|
|
890
842
|
default_history_content,
|
|
891
843
|
)
|
|
892
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
|
+
|
|
893
893
|
def add_ancestor_states_to_enter(
|
|
894
894
|
self,
|
|
895
895
|
info: StateTransition,
|
|
@@ -952,9 +952,10 @@ class BaseEngine:
|
|
|
952
952
|
self.running = False
|
|
953
953
|
|
|
954
954
|
def is_in_final_state(self, state: State) -> bool:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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:
|
|
958
960
|
return all(self.is_in_final_state(s) for s in state.states)
|
|
959
|
-
|
|
960
|
-
return False
|
|
961
|
+
return any(s.final and s in self.sm.configuration for s in state.states)
|