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/sync.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from time import monotonic
|
|
1
4
|
from time import sleep
|
|
2
|
-
from time import time
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from statemachine.event import BoundEvent
|
|
6
7
|
from statemachine.orderedset import OrderedSet
|
|
@@ -8,26 +9,13 @@ from statemachine.orderedset import OrderedSet
|
|
|
8
9
|
from ..event_data import TriggerData
|
|
9
10
|
from ..exceptions import InvalidDefinition
|
|
10
11
|
from ..exceptions import TransitionNotAllowed
|
|
12
|
+
from ..state import State
|
|
13
|
+
from ..transition import Transition
|
|
11
14
|
from .base import BaseEngine
|
|
12
|
-
|
|
13
|
-
if TYPE_CHECKING:
|
|
14
|
-
from ..transition import Transition
|
|
15
|
+
from .base import _processing_step
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SyncEngine(BaseEngine):
|
|
18
|
-
def _run_microstep(self, enabled_transitions, trigger_data):
|
|
19
|
-
"""Run a microstep for internal/eventless transitions with error handling.
|
|
20
|
-
|
|
21
|
-
Note: microstep() handles its own errors internally, so this try/except
|
|
22
|
-
is a safety net that is not expected to be reached in normal operation.
|
|
23
|
-
"""
|
|
24
|
-
try:
|
|
25
|
-
self.microstep(list(enabled_transitions), trigger_data)
|
|
26
|
-
except InvalidDefinition:
|
|
27
|
-
raise
|
|
28
|
-
except Exception as e: # pragma: no cover
|
|
29
|
-
self._handle_error(e, trigger_data)
|
|
30
|
-
|
|
31
19
|
def start(self, **kwargs):
|
|
32
20
|
if self.sm.current_state_value is not None:
|
|
33
21
|
return
|
|
@@ -51,12 +39,13 @@ class SyncEngine(BaseEngine):
|
|
|
51
39
|
transitions = self._initial_transitions(trigger_data)
|
|
52
40
|
self._processing.acquire(blocking=False)
|
|
53
41
|
try:
|
|
54
|
-
|
|
42
|
+
with _processing_step(self):
|
|
43
|
+
self._enter_states(transitions, trigger_data, OrderedSet(), OrderedSet())
|
|
55
44
|
finally:
|
|
56
45
|
self._processing.release()
|
|
57
46
|
return self.processing_loop()
|
|
58
47
|
|
|
59
|
-
def processing_loop(self
|
|
48
|
+
def processing_loop(self):
|
|
60
49
|
"""Process event triggers.
|
|
61
50
|
|
|
62
51
|
The event is put on a queue, and only the first event will have the result collected.
|
|
@@ -77,104 +66,231 @@ class SyncEngine(BaseEngine):
|
|
|
77
66
|
self._debug("%s Processing loop started: %s", self._log_id, self.sm.current_state_value)
|
|
78
67
|
first_result = self._sentinel
|
|
79
68
|
try:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self.
|
|
83
|
-
took_events = False
|
|
84
|
-
# Execute the triggers in the queue in FIFO order until the queue is empty
|
|
85
|
-
# while self._running and not self.external_queue.is_empty():
|
|
86
|
-
macrostep_done = False
|
|
87
|
-
enabled_transitions: "OrderedSet[Transition] | None" = None
|
|
88
|
-
|
|
89
|
-
# handles eventless transitions and internal events
|
|
90
|
-
while not macrostep_done:
|
|
91
|
-
self._microstep_count = 0
|
|
92
|
-
self._debug(
|
|
93
|
-
"%s Macrostep %d: eventless/internal queue",
|
|
94
|
-
self._log_id,
|
|
95
|
-
self._macrostep_count,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
self.clear_cache()
|
|
99
|
-
internal_event = TriggerData(
|
|
100
|
-
self.sm, event=None
|
|
101
|
-
) # this one is a "null object"
|
|
102
|
-
enabled_transitions = self.select_eventless_transitions(internal_event)
|
|
103
|
-
if not enabled_transitions:
|
|
104
|
-
if self.internal_queue.is_empty():
|
|
105
|
-
macrostep_done = True
|
|
106
|
-
else:
|
|
107
|
-
internal_event = self.internal_queue.pop()
|
|
108
|
-
enabled_transitions = self.select_transitions(internal_event)
|
|
109
|
-
if enabled_transitions:
|
|
110
|
-
self._debug(
|
|
111
|
-
"%s Enabled transitions: %s", self._log_id, enabled_transitions
|
|
112
|
-
)
|
|
113
|
-
took_events = True
|
|
114
|
-
self._run_microstep(enabled_transitions, internal_event)
|
|
115
|
-
|
|
116
|
-
# Spawn invoke handlers for states entered during this macrostep.
|
|
117
|
-
self._invoke_manager.spawn_pending_sync()
|
|
118
|
-
self._check_root_final_state()
|
|
119
|
-
|
|
120
|
-
# Process remaining internal events before external events.
|
|
121
|
-
# Note: the macrostep loop above already drains the internal queue,
|
|
122
|
-
# so this is a safety net per SCXML spec for invoke-generated events.
|
|
123
|
-
while not self.internal_queue.is_empty(): # pragma: no cover
|
|
124
|
-
internal_event = self.internal_queue.pop()
|
|
125
|
-
enabled_transitions = self.select_transitions(internal_event)
|
|
126
|
-
if enabled_transitions:
|
|
127
|
-
self._run_microstep(enabled_transitions, internal_event)
|
|
128
|
-
|
|
129
|
-
# Process external events
|
|
130
|
-
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
|
|
131
|
-
while not self.external_queue.is_empty():
|
|
69
|
+
with _processing_step(self):
|
|
70
|
+
took_events = True
|
|
71
|
+
while took_events and self.running:
|
|
132
72
|
self.clear_cache()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
73
|
+
try:
|
|
74
|
+
took_events = self._run_macrostep()
|
|
75
|
+
# Spawn invoke handlers for states entered during this macrostep.
|
|
76
|
+
self._invoke_manager.spawn_pending_sync()
|
|
77
|
+
except Exception:
|
|
78
|
+
self.clear()
|
|
79
|
+
raise
|
|
80
|
+
self._check_root_final_state()
|
|
81
|
+
if not self.running:
|
|
141
82
|
break
|
|
142
83
|
|
|
143
|
-
self.
|
|
144
|
-
|
|
145
|
-
self._debug(
|
|
146
|
-
"%s macrostep %d: event=%s",
|
|
147
|
-
self._log_id,
|
|
148
|
-
self._macrostep_count,
|
|
149
|
-
external_event.event,
|
|
84
|
+
took_external_events, first_result = self._take_due_external_event(
|
|
85
|
+
first_result
|
|
150
86
|
)
|
|
87
|
+
took_events = took_events or took_external_events
|
|
151
88
|
|
|
152
|
-
|
|
153
|
-
self.
|
|
154
|
-
|
|
155
|
-
enabled_transitions = self.select_transitions(external_event)
|
|
156
|
-
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
|
|
157
|
-
if enabled_transitions:
|
|
158
|
-
try:
|
|
159
|
-
result = self.microstep(list(enabled_transitions), external_event)
|
|
160
|
-
if first_result is self._sentinel:
|
|
161
|
-
first_result = result
|
|
162
|
-
|
|
163
|
-
except Exception:
|
|
164
|
-
# We clear the queue as we don't have an expected behavior
|
|
165
|
-
# and cannot keep processing
|
|
166
|
-
self.clear()
|
|
167
|
-
raise
|
|
168
|
-
|
|
169
|
-
else:
|
|
170
|
-
if not self.sm.allow_event_without_transition:
|
|
171
|
-
raise TransitionNotAllowed(external_event.event, self.sm.configuration)
|
|
172
|
-
|
|
89
|
+
if not self.running:
|
|
90
|
+
self._drop_external_events()
|
|
173
91
|
finally:
|
|
174
92
|
self._processing.release()
|
|
175
93
|
self._debug("%s Processing loop ended", self._log_id)
|
|
176
94
|
return first_result if first_result is not self._sentinel else None
|
|
177
95
|
|
|
96
|
+
def _run_macrostep(self) -> bool:
|
|
97
|
+
"""Take eventless transitions and internal events until neither enables a transition.
|
|
98
|
+
|
|
99
|
+
Returns whether any transition was taken. The internal queue is empty on return.
|
|
100
|
+
"""
|
|
101
|
+
took_events = False
|
|
102
|
+
while True:
|
|
103
|
+
self._microstep_count = 0
|
|
104
|
+
self._debug(
|
|
105
|
+
"%s Macrostep %d: eventless/internal queue",
|
|
106
|
+
self._log_id,
|
|
107
|
+
self._macrostep_count,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.clear_cache()
|
|
111
|
+
internal_event = TriggerData(self.sm, event=None) # this one is a "null object"
|
|
112
|
+
enabled_transitions = self.select_eventless_transitions(internal_event)
|
|
113
|
+
if not enabled_transitions:
|
|
114
|
+
if self.internal_queue.is_empty():
|
|
115
|
+
return took_events
|
|
116
|
+
internal_event = self.internal_queue.pop()
|
|
117
|
+
enabled_transitions = self.select_transitions(internal_event)
|
|
118
|
+
if enabled_transitions:
|
|
119
|
+
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
|
|
120
|
+
took_events = True
|
|
121
|
+
self.microstep(list(enabled_transitions), internal_event)
|
|
122
|
+
|
|
123
|
+
def _take_due_external_event(self, first_result):
|
|
124
|
+
"""Take the external event at the head of the queue, if it is due.
|
|
125
|
+
|
|
126
|
+
One at a time, so the macrostep that follows runs the internal events and eventless
|
|
127
|
+
transitions it enabled before the next external event is taken.
|
|
128
|
+
|
|
129
|
+
Returns whether the queue held an event, and ``first_result`` updated with the result
|
|
130
|
+
of the first event that enabled a transition.
|
|
131
|
+
"""
|
|
132
|
+
self._debug("%s Macrostep %d: external queue", self._log_id, self._macrostep_count)
|
|
133
|
+
if self.external_queue.is_empty():
|
|
134
|
+
return False, first_result
|
|
135
|
+
|
|
136
|
+
self.clear_cache()
|
|
137
|
+
external_event = self.external_queue.pop_due(monotonic())
|
|
138
|
+
if external_event is None:
|
|
139
|
+
sleep(self.sm._loop_sleep_in_ms)
|
|
140
|
+
return True, first_result
|
|
141
|
+
|
|
142
|
+
self._macrostep_count += 1
|
|
143
|
+
self._microstep_count = 0
|
|
144
|
+
self._debug(
|
|
145
|
+
"%s macrostep %d: event=%s",
|
|
146
|
+
self._log_id,
|
|
147
|
+
self._macrostep_count,
|
|
148
|
+
external_event.event,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
result = self._take_external_event(external_event)
|
|
152
|
+
if first_result is self._sentinel:
|
|
153
|
+
first_result = result
|
|
154
|
+
return True, first_result
|
|
155
|
+
|
|
156
|
+
def _take_external_event(self, external_event: TriggerData):
|
|
157
|
+
"""Run one external event.
|
|
158
|
+
|
|
159
|
+
Returns the event's microstep result, or the sentinel when it enabled no transition.
|
|
160
|
+
"""
|
|
161
|
+
# Finalize + autoforward for active invocations
|
|
162
|
+
self._invoke_manager.handle_external_event(external_event)
|
|
163
|
+
|
|
164
|
+
enabled_transitions = self.select_transitions(external_event)
|
|
165
|
+
self._debug("%s Enabled transitions: %s", self._log_id, enabled_transitions)
|
|
166
|
+
if not enabled_transitions:
|
|
167
|
+
if not self.sm.allow_event_without_transition:
|
|
168
|
+
raise TransitionNotAllowed(external_event.event, self.sm.configuration)
|
|
169
|
+
return self._sentinel
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
return self.microstep(list(enabled_transitions), external_event)
|
|
173
|
+
except Exception:
|
|
174
|
+
# We clear the queue as we don't have an expected behavior
|
|
175
|
+
# and cannot keep processing
|
|
176
|
+
self.clear()
|
|
177
|
+
raise
|
|
178
|
+
|
|
179
|
+
def select_eventless_transitions(self, trigger_data: TriggerData):
|
|
180
|
+
"""
|
|
181
|
+
Select the eventless transitions that match the trigger data.
|
|
182
|
+
"""
|
|
183
|
+
return self._select_transitions(trigger_data, lambda t, _e: t.is_eventless)
|
|
184
|
+
|
|
185
|
+
def select_transitions(self, trigger_data: TriggerData) -> OrderedSet[Transition]:
|
|
186
|
+
"""
|
|
187
|
+
Select the transitions that match the trigger data.
|
|
188
|
+
"""
|
|
189
|
+
return self._select_transitions(trigger_data, lambda t, e: t.match(e))
|
|
190
|
+
|
|
191
|
+
def _first_transition_that_matches(
|
|
192
|
+
self,
|
|
193
|
+
state: State,
|
|
194
|
+
trigger_data: TriggerData,
|
|
195
|
+
predicate: Callable,
|
|
196
|
+
) -> "Transition | None":
|
|
197
|
+
for s in chain([state], state.ancestors()):
|
|
198
|
+
transition: Transition
|
|
199
|
+
for transition in s.transitions:
|
|
200
|
+
if (
|
|
201
|
+
not transition.initial
|
|
202
|
+
and predicate(transition, trigger_data.event)
|
|
203
|
+
and self._conditions_match(transition, trigger_data)
|
|
204
|
+
):
|
|
205
|
+
return transition
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def _select_transitions(
|
|
209
|
+
self, trigger_data: TriggerData, predicate: Callable
|
|
210
|
+
) -> OrderedSet[Transition]:
|
|
211
|
+
"""Select the transitions that match the trigger data."""
|
|
212
|
+
enabled_transitions = OrderedSet[Transition]()
|
|
213
|
+
|
|
214
|
+
# Get atomic states, TODO: sorted by document order
|
|
215
|
+
atomic_states = (state for state in self.sm.configuration if state.is_atomic)
|
|
216
|
+
|
|
217
|
+
for state in atomic_states:
|
|
218
|
+
transition = self._first_transition_that_matches(state, trigger_data, predicate)
|
|
219
|
+
if transition is not None:
|
|
220
|
+
enabled_transitions.add(transition)
|
|
221
|
+
|
|
222
|
+
return self._filter_conflicting_transitions(enabled_transitions)
|
|
223
|
+
|
|
224
|
+
def microstep(self, transitions: list[Transition], trigger_data: TriggerData):
|
|
225
|
+
"""Process a single set of transitions in a 'lock step'.
|
|
226
|
+
This includes exiting states, executing transition content, and entering states.
|
|
227
|
+
"""
|
|
228
|
+
self._microstep_count += 1
|
|
229
|
+
self._debug(
|
|
230
|
+
"%s macro:%d micro:%d transitions: %s",
|
|
231
|
+
self._log_id,
|
|
232
|
+
self._macrostep_count,
|
|
233
|
+
self._microstep_count,
|
|
234
|
+
transitions,
|
|
235
|
+
)
|
|
236
|
+
previous_configuration = self.sm.configuration
|
|
237
|
+
try:
|
|
238
|
+
result = self._execute_transition_content(
|
|
239
|
+
transitions, trigger_data, lambda t: t.before.key
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
states_to_exit = self._exit_states(transitions, trigger_data)
|
|
243
|
+
result += self._enter_states(
|
|
244
|
+
transitions, trigger_data, states_to_exit, previous_configuration
|
|
245
|
+
)
|
|
246
|
+
except InvalidDefinition:
|
|
247
|
+
self.sm.configuration = previous_configuration
|
|
248
|
+
raise
|
|
249
|
+
except Exception as e:
|
|
250
|
+
self.sm.configuration = previous_configuration
|
|
251
|
+
self._handle_error(e, trigger_data)
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
self._execute_transition_content(
|
|
256
|
+
transitions,
|
|
257
|
+
trigger_data,
|
|
258
|
+
lambda t: t.after.key,
|
|
259
|
+
set_target_as_state=True,
|
|
260
|
+
)
|
|
261
|
+
except InvalidDefinition:
|
|
262
|
+
raise
|
|
263
|
+
except Exception as e:
|
|
264
|
+
self._handle_error(e, trigger_data)
|
|
265
|
+
|
|
266
|
+
if len(result) == 0:
|
|
267
|
+
result = None
|
|
268
|
+
elif len(result) == 1:
|
|
269
|
+
result = result[0]
|
|
270
|
+
|
|
271
|
+
return result
|
|
272
|
+
|
|
273
|
+
def _exit_states(
|
|
274
|
+
self, enabled_transitions: list[Transition], trigger_data: TriggerData
|
|
275
|
+
) -> OrderedSet[State]:
|
|
276
|
+
"""Compute and process the states to exit for the given transitions."""
|
|
277
|
+
ordered_states, result = self._prepare_exit_states(enabled_transitions)
|
|
278
|
+
on_error = self._on_error_handler()
|
|
279
|
+
|
|
280
|
+
for info in ordered_states:
|
|
281
|
+
# Cancel invocations for this state before executing exit handlers.
|
|
282
|
+
self._invoke_manager.cancel_for_state(info.state)
|
|
283
|
+
|
|
284
|
+
args, kwargs = self._get_args_kwargs(info.transition, trigger_data, source=info.state)
|
|
285
|
+
|
|
286
|
+
# Execute `onexit` handlers — same per-block error isolation as onentry.
|
|
287
|
+
self._debug("%s Exiting state: %s", self._log_id, info.state)
|
|
288
|
+
self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs)
|
|
289
|
+
|
|
290
|
+
self._remove_state_from_configuration(info.state)
|
|
291
|
+
|
|
292
|
+
return result
|
|
293
|
+
|
|
178
294
|
def enabled_events(self, *args, **kwargs):
|
|
179
295
|
sm = self.sm
|
|
180
296
|
enabled = {}
|
statemachine/event.py
CHANGED
|
@@ -16,22 +16,22 @@ if TYPE_CHECKING:
|
|
|
16
16
|
from .transition_list import TransitionList
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _expand_event_id(key: str) -> str:
|
|
20
|
-
"""
|
|
19
|
+
def _expand_event_id(key: str) -> list[str]:
|
|
20
|
+
"""Return the event ids an attribute name declares.
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
``done_invoke_X``, ``done_state_X`` and ``error_X`` also declare their SCXML dotted form
|
|
23
|
+
(``done.invoke.X``, ``done.state.X``, ``error.X``), each registered as its own event. Any
|
|
24
|
+
other name declares only itself.
|
|
25
25
|
"""
|
|
26
26
|
if key.startswith("done_invoke_"):
|
|
27
27
|
suffix = key[len("done_invoke_") :]
|
|
28
|
-
return f"
|
|
28
|
+
return [key, f"done.invoke.{suffix}"]
|
|
29
29
|
if key.startswith("done_state_"):
|
|
30
30
|
suffix = key[len("done_state_") :]
|
|
31
|
-
return f"
|
|
31
|
+
return [key, f"done.state.{suffix}"]
|
|
32
32
|
if key.startswith("error_"):
|
|
33
|
-
return
|
|
34
|
-
return key
|
|
33
|
+
return [key, key.replace("_", ".")]
|
|
34
|
+
return [key]
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
_event_data_kwargs = {
|
|
@@ -46,6 +46,20 @@ _event_data_kwargs = {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def raise_delayed_internal(event_id: str):
|
|
50
|
+
"""Reject an event that asks to be both delayed and internal.
|
|
51
|
+
|
|
52
|
+
The internal queue runs within the current macrostep, which takes no time.
|
|
53
|
+
"""
|
|
54
|
+
raise InvalidDefinition(
|
|
55
|
+
_(
|
|
56
|
+
"Event {id!r} cannot be both delayed and internal: the internal queue runs "
|
|
57
|
+
"within the current macrostep, which takes no time. Drop 'internal' to "
|
|
58
|
+
"deliver it later on the external queue, or drop 'delay' to run it now."
|
|
59
|
+
).format(id=event_id)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
49
63
|
class Event(AddCallbacksMixin, str):
|
|
50
64
|
"""An event triggers a signal that something has happened.
|
|
51
65
|
|
|
@@ -100,6 +114,9 @@ class Event(AddCallbacksMixin, str):
|
|
|
100
114
|
_has_real_id = id is not None
|
|
101
115
|
id = str(id) if _has_real_id else f"__event__{uuid4().hex}"
|
|
102
116
|
|
|
117
|
+
if delay and internal and _has_real_id:
|
|
118
|
+
raise_delayed_internal(id)
|
|
119
|
+
|
|
103
120
|
instance = super().__new__(cls, id)
|
|
104
121
|
instance.id = id
|
|
105
122
|
instance.delay = delay
|
|
@@ -146,7 +163,9 @@ class Event(AddCallbacksMixin, str):
|
|
|
146
163
|
"""
|
|
147
164
|
if instance is None:
|
|
148
165
|
return self
|
|
149
|
-
return BoundEvent(
|
|
166
|
+
return BoundEvent(
|
|
167
|
+
id=self.id, name=self.name, delay=self.delay, internal=self.internal, _sm=instance
|
|
168
|
+
)
|
|
150
169
|
|
|
151
170
|
def put(self, *args, send_id: "str | None" = None, **kwargs):
|
|
152
171
|
# The `__call__` is declared here to help IDEs knowing that an `Event`
|
statemachine/event_data.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from dataclasses import field
|
|
3
|
-
from
|
|
3
|
+
from itertools import count
|
|
4
|
+
from time import monotonic
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
from typing import Any
|
|
6
7
|
|
|
@@ -10,6 +11,9 @@ if TYPE_CHECKING:
|
|
|
10
11
|
from .statemachine import StateChart
|
|
11
12
|
from .transition import Transition
|
|
12
13
|
|
|
14
|
+
_sequence = count()
|
|
15
|
+
"""Numbers each trigger as it is created, so triggers due at the same time keep their order."""
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
@dataclass(order=True)
|
|
15
19
|
class TriggerData:
|
|
@@ -25,7 +29,13 @@ class TriggerData:
|
|
|
25
29
|
"""
|
|
26
30
|
|
|
27
31
|
execution_time: float = field(default=0.0)
|
|
28
|
-
"""The time at which the :ref:`Event` should run.
|
|
32
|
+
"""The time at which the :ref:`Event` should run, on the :func:`time.monotonic` clock, so
|
|
33
|
+
that a step of the wall clock neither holds nor hastens it."""
|
|
34
|
+
|
|
35
|
+
sequence: int = field(default=0, init=False)
|
|
36
|
+
"""The order this trigger was created in, which breaks a tie between triggers due at the
|
|
37
|
+
same time: the queues are priority queues, and without it two triggers stamped alike come
|
|
38
|
+
out in no particular order."""
|
|
29
39
|
|
|
30
40
|
model: Any = field(init=False, compare=False)
|
|
31
41
|
"""A reference to the underlying model that holds the current :ref:`State`."""
|
|
@@ -43,10 +53,16 @@ class TriggerData:
|
|
|
43
53
|
result (or exception), allowing the caller to ``await`` it.
|
|
44
54
|
"""
|
|
45
55
|
|
|
56
|
+
cancelled: bool = field(default=False, compare=False, init=False)
|
|
57
|
+
"""Whether :meth:`~statemachine.StateChart.cancel_event` cancelled this trigger. A cancelled
|
|
58
|
+
trigger stays in its queue, so the triggers around it keep their due order, and is dropped
|
|
59
|
+
when it reaches the head."""
|
|
60
|
+
|
|
46
61
|
def __post_init__(self):
|
|
47
62
|
self.model = self.machine.model
|
|
48
63
|
delay = self.event.delay if self.event and self.event.delay else 0
|
|
49
|
-
self.execution_time =
|
|
64
|
+
self.execution_time = monotonic() + (delay / 1000)
|
|
65
|
+
self.sequence = next(_sequence)
|
|
50
66
|
|
|
51
67
|
|
|
52
68
|
@dataclass
|
statemachine/events.py
CHANGED
|
@@ -18,6 +18,9 @@ class Events:
|
|
|
18
18
|
def __iter__(self):
|
|
19
19
|
return iter(self._items)
|
|
20
20
|
|
|
21
|
+
def __contains__(self, event):
|
|
22
|
+
return event in self._items
|
|
23
|
+
|
|
21
24
|
def add(self, events):
|
|
22
25
|
if events is None:
|
|
23
26
|
return self
|
|
@@ -39,9 +42,9 @@ class Events:
|
|
|
39
42
|
return self.is_empty
|
|
40
43
|
return any(e.match(event) for e in self)
|
|
41
44
|
|
|
42
|
-
def _replace(self, old,
|
|
45
|
+
def _replace(self, old, new_events):
|
|
43
46
|
self._items.remove(old)
|
|
44
|
-
self.
|
|
47
|
+
self.add(new_events)
|
|
45
48
|
|
|
46
49
|
@property
|
|
47
50
|
def is_empty(self):
|