Skip to content

Commit c68741d

Browse files
committed
refactor: unify error.execution handling in engines
Replace scattered `if self.sm.error_on_execution` checks and the broken `_on_error_execution` property with two unified methods: - `_on_error_handler(trigger_data)`: returns a bound callable (or None) for per-block callback error isolation (onentry/onexit/conditions). - `_handle_error(error, trigger_data)`: for try/except blocks in microstep/_run_microstep — sends error.execution or re-raises. Fixes two bugs in the previous code: - base.py used `partial(self._on_error_execution, trigger_data)` which bound trigger_data to the wrong positional parameter (error), causing AttributeError on any per-block callback error. - async_.py passed `self._on_error_execution` without binding trigger_data at all, which would fail with a missing argument. Also fixes `_send_error_execution` parameter order to (error, trigger_data) and removes the unused `functools.partial` import.
1 parent ecbc140 commit c68741d

3 files changed

Lines changed: 50 additions & 50 deletions

File tree

statemachine/engines/async_.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ async def _get_args_kwargs(
5454

5555
async def _conditions_match(self, transition: "Transition", trigger_data: TriggerData):
5656
args, kwargs = await self._get_args_kwargs(transition, trigger_data)
57+
on_error = self._on_error_handler(trigger_data)
5758

5859
await self.sm._callbacks.async_call(
59-
transition.validators.key, *args, on_error=self._on_error_execution, **kwargs
60+
transition.validators.key, *args, on_error=on_error, **kwargs
6061
)
6162
return await self.sm._callbacks.async_all(
62-
transition.cond.key, *args, on_error=self._on_error_execution, **kwargs
63+
transition.cond.key, *args, on_error=on_error, **kwargs
6364
)
6465

6566
async def _select_transitions( # type: ignore[override]
@@ -122,13 +123,14 @@ async def _exit_states( # type: ignore[override]
122123
self, enabled_transitions: "List[Transition]", trigger_data: TriggerData
123124
) -> "OrderedSet[State]":
124125
ordered_states, result = self._prepare_exit_states(enabled_transitions)
126+
on_error = self._on_error_handler(trigger_data)
125127

126128
for info in ordered_states:
127129
args, kwargs = await self._get_args_kwargs(info.transition, trigger_data)
128130

129131
if info.state is not None:
130132
await self.sm._callbacks.async_call(
131-
info.state.exit.key, *args, on_error=self._on_error_execution, **kwargs
133+
info.state.exit.key, *args, on_error=on_error, **kwargs
132134
)
133135

134136
self._remove_state_from_configuration(info.state)
@@ -142,6 +144,7 @@ async def _enter_states( # noqa: C901
142144
states_to_exit: "OrderedSet[State]",
143145
previous_configuration: "OrderedSet[State]",
144146
):
147+
on_error = self._on_error_handler(trigger_data)
145148
ordered_states, states_for_default_entry, default_history_content, new_configuration = (
146149
self._prepare_entry_states(enabled_transitions, states_to_exit, previous_configuration)
147150
)
@@ -170,7 +173,7 @@ async def _enter_states( # noqa: C901
170173
self._add_state_to_configuration(target)
171174

172175
on_entry_result = await self.sm._callbacks.async_call(
173-
target.enter.key, *args, on_error=self._on_error_execution, **kwargs
176+
target.enter.key, *args, on_error=on_error, **kwargs
174177
)
175178

176179
# Handle default initial states
@@ -216,10 +219,8 @@ async def microstep(self, transitions: "List[Transition]", trigger_data: Trigger
216219
raise
217220
except Exception as e:
218221
self.sm.configuration = previous_configuration
219-
if self.sm.error_on_execution:
220-
self._send_error_execution(trigger_data, e)
221-
return None
222-
raise
222+
self._handle_error(e, trigger_data)
223+
return None
223224

224225
try:
225226
await self._execute_transition_content(
@@ -231,10 +232,7 @@ async def microstep(self, transitions: "List[Transition]", trigger_data: Trigger
231232
except InvalidDefinition:
232233
raise
233234
except Exception as e:
234-
if self.sm.error_on_execution:
235-
self._send_error_execution(trigger_data, e)
236-
else:
237-
raise
235+
self._handle_error(e, trigger_data)
238236

239237
if len(result) == 0:
240238
result = None
@@ -252,10 +250,7 @@ async def _run_microstep(self, enabled_transitions, trigger_data):
252250
except InvalidDefinition:
253251
raise
254252
except Exception as e: # pragma: no cover
255-
if self.sm.error_on_execution:
256-
self._send_error_execution(trigger_data, e)
257-
else:
258-
raise
253+
self._handle_error(e, trigger_data)
259254

260255
async def activate_initial_state(self):
261256
"""Activate the initial state.

statemachine/engines/base.py

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -120,29 +120,45 @@ def cancel_event(self, send_id: str):
120120
"""Cancel the event with the given send_id."""
121121
self.external_queue.remove(send_id)
122122

123-
@property
124-
def _on_error_execution(self):
125-
"""Return an error handler for per-block error isolation, or None.
123+
def _on_error_handler(self, trigger_data: TriggerData) -> "Callable[[Exception], None] | None":
124+
"""Return a per-block error handler bound to *trigger_data*, or ``None``.
126125
127-
When ``error_on_execution`` is enabled, returns a handler that queues
126+
When ``error_on_execution`` is enabled, returns a callable that queues
128127
``error.execution`` on the internal queue. Otherwise returns ``None``
129128
so that exceptions propagate normally.
130129
"""
131-
if self.sm.error_on_execution:
130+
if not self.sm.error_on_execution:
131+
return None
132132

133-
def _handler(e: Exception):
134-
if isinstance(e, InvalidDefinition):
135-
raise
136-
self.sm.send("error.execution", error=e, internal=True)
133+
def handler(error: Exception) -> None:
134+
if isinstance(error, InvalidDefinition):
135+
raise error
136+
# Per-block errors always queue error.execution — even when the current
137+
# event is itself error.execution. The SCXML spec mandates that the
138+
# new error.execution is a separate event that may trigger a different
139+
# transition (see W3C test 152). The infinite-loop guard lives at the
140+
# *microstep* level (in ``_send_error_execution``), not here.
141+
self.sm.send("error.execution", error=error, internal=True)
137142

138-
return _handler
139-
return None
143+
return handler
140144

141-
def _send_error_execution(self, trigger_data: TriggerData, error: Exception):
145+
def _handle_error(self, error: Exception, trigger_data: TriggerData):
146+
"""Handle an execution error: send ``error.execution`` or re-raise.
147+
148+
Centralises the ``if error_on_execution`` check so callers don't need
149+
to know about the variation.
150+
"""
151+
if self.sm.error_on_execution:
152+
self._send_error_execution(error, trigger_data)
153+
else:
154+
raise error
155+
156+
def _send_error_execution(self, error: Exception, trigger_data: TriggerData):
142157
"""Send error.execution to internal queue (SCXML spec).
143158
144159
If already processing an error.execution event, ignore to avoid infinite loops.
145160
"""
161+
logger.debug("Error %s captured while executing event=%s", error, trigger_data.event)
146162
if trigger_data.event and str(trigger_data.event) == "error.execution":
147163
logger.warning("Error while processing error.execution, ignoring: %s", error)
148164
return
@@ -353,10 +369,8 @@ def microstep(self, transitions: List[Transition], trigger_data: TriggerData):
353369
raise
354370
except Exception as e:
355371
self.sm.configuration = previous_configuration
356-
if self.sm.error_on_execution:
357-
self._send_error_execution(trigger_data, e)
358-
return None
359-
raise
372+
self._handle_error(e, trigger_data)
373+
return None
360374

361375
try:
362376
self._execute_transition_content(
@@ -368,10 +382,7 @@ def microstep(self, transitions: List[Transition], trigger_data: TriggerData):
368382
except InvalidDefinition:
369383
raise
370384
except Exception as e:
371-
if self.sm.error_on_execution:
372-
self._send_error_execution(trigger_data, e)
373-
else:
374-
raise
385+
self._handle_error(e, trigger_data)
375386

376387
if len(result) == 0:
377388
result = None
@@ -407,13 +418,10 @@ def _get_args_kwargs(
407418

408419
def _conditions_match(self, transition: Transition, trigger_data: TriggerData):
409420
args, kwargs = self._get_args_kwargs(transition, trigger_data)
421+
on_error = self._on_error_handler(trigger_data)
410422

411-
self.sm._callbacks.call(
412-
transition.validators.key, *args, on_error=self._on_error_execution, **kwargs
413-
)
414-
return self.sm._callbacks.all(
415-
transition.cond.key, *args, on_error=self._on_error_execution, **kwargs
416-
)
423+
self.sm._callbacks.call(transition.validators.key, *args, on_error=on_error, **kwargs)
424+
return self.sm._callbacks.all(transition.cond.key, *args, on_error=on_error, **kwargs)
417425

418426
def _prepare_exit_states(
419427
self,
@@ -457,15 +465,14 @@ def _exit_states(
457465
) -> OrderedSet[State]:
458466
"""Compute and process the states to exit for the given transitions."""
459467
ordered_states, result = self._prepare_exit_states(enabled_transitions)
468+
on_error = self._on_error_handler(trigger_data)
460469

461470
for info in ordered_states:
462471
args, kwargs = self._get_args_kwargs(info.transition, trigger_data)
463472

464473
# Execute `onexit` handlers — same per-block error isolation as onentry.
465474
if info.state is not None: # TODO: and not info.transition.internal:
466-
self.sm._callbacks.call(
467-
info.state.exit.key, *args, on_error=self._on_error_execution, **kwargs
468-
)
475+
self.sm._callbacks.call(info.state.exit.key, *args, on_error=on_error, **kwargs)
469476

470477
self._remove_state_from_configuration(info.state)
471478

@@ -568,6 +575,7 @@ def _enter_states( # noqa: C901
568575
previous_configuration: OrderedSet[State],
569576
):
570577
"""Enter the states as determined by the given transitions."""
578+
on_error = self._on_error_handler(trigger_data)
571579
ordered_states, states_for_default_entry, default_history_content, new_configuration = (
572580
self._prepare_entry_states(enabled_transitions, states_to_exit, previous_configuration)
573581
)
@@ -598,7 +606,7 @@ def _enter_states( # noqa: C901
598606
# Execute `onentry` handlers — each handler is a separate block per
599607
# SCXML spec: errors in one block MUST NOT affect other blocks.
600608
on_entry_result = self.sm._callbacks.call(
601-
target.enter.key, *args, on_error=self._on_error_execution, **kwargs
609+
target.enter.key, *args, on_error=on_error, **kwargs
602610
)
603611

604612
# Handle default initial states

statemachine/engines/sync.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,7 @@ def _run_microstep(self, enabled_transitions, trigger_data):
2929
except InvalidDefinition:
3030
raise
3131
except Exception as e: # pragma: no cover
32-
if self.sm.error_on_execution:
33-
self._send_error_execution(trigger_data, e)
34-
else:
35-
raise
32+
self._handle_error(e, trigger_data)
3633

3734
def start(self):
3835
if self.sm.current_state_value is not None:

0 commit comments

Comments
 (0)