|
18 | 18 | from ..event import Event |
19 | 19 | from ..event_data import EventData |
20 | 20 | from ..event_data import TriggerData |
| 21 | +from ..exceptions import InvalidDefinition |
21 | 22 | from ..exceptions import TransitionNotAllowed |
22 | 23 | from ..orderedset import OrderedSet |
23 | 24 | from ..state import HistoryState |
@@ -119,6 +120,34 @@ def cancel_event(self, send_id: str): |
119 | 120 | """Cancel the event with the given send_id.""" |
120 | 121 | self.external_queue.remove(send_id) |
121 | 122 |
|
| 123 | + @property |
| 124 | + def _on_error_execution(self): |
| 125 | + """Return an error handler for per-block error isolation, or None. |
| 126 | +
|
| 127 | + When ``error_on_execution`` is enabled, returns a handler that queues |
| 128 | + ``error.execution`` on the internal queue. Otherwise returns ``None`` |
| 129 | + so that exceptions propagate normally. |
| 130 | + """ |
| 131 | + if self.sm.error_on_execution: |
| 132 | + |
| 133 | + def _handler(e: Exception): |
| 134 | + if isinstance(e, InvalidDefinition): |
| 135 | + raise |
| 136 | + self.sm.send("error.execution", error=e, internal=True) |
| 137 | + |
| 138 | + return _handler |
| 139 | + return None |
| 140 | + |
| 141 | + def _send_error_execution(self, trigger_data: TriggerData, error: Exception): |
| 142 | + """Send error.execution to internal queue (SCXML spec). |
| 143 | +
|
| 144 | + If already processing an error.execution event, ignore to avoid infinite loops. |
| 145 | + """ |
| 146 | + if trigger_data.event and str(trigger_data.event) == "error.execution": |
| 147 | + logger.warning("Error while processing error.execution, ignoring: %s", error) |
| 148 | + return |
| 149 | + self.sm.send("error.execution", error=error, internal=True) |
| 150 | + |
122 | 151 | def start(self): |
123 | 152 | if self.sm.current_state_value is not None: |
124 | 153 | return |
@@ -321,15 +350,30 @@ def microstep(self, transitions: List[Transition], trigger_data: TriggerData): |
321 | 350 | result += self._enter_states( |
322 | 351 | transitions, trigger_data, states_to_exit, previous_configuration |
323 | 352 | ) |
324 | | - except Exception: |
| 353 | + except InvalidDefinition: |
325 | 354 | self.sm.configuration = previous_configuration |
326 | 355 | raise |
327 | | - self._execute_transition_content( |
328 | | - transitions, |
329 | | - trigger_data, |
330 | | - lambda t: t.after.key, |
331 | | - set_target_as_state=True, |
332 | | - ) |
| 356 | + except Exception as e: |
| 357 | + self.sm.configuration = previous_configuration |
| 358 | + if self.sm.error_on_execution: |
| 359 | + self._send_error_execution(trigger_data, e) |
| 360 | + return None |
| 361 | + raise |
| 362 | + |
| 363 | + try: |
| 364 | + self._execute_transition_content( |
| 365 | + transitions, |
| 366 | + trigger_data, |
| 367 | + lambda t: t.after.key, |
| 368 | + set_target_as_state=True, |
| 369 | + ) |
| 370 | + except InvalidDefinition: |
| 371 | + raise |
| 372 | + except Exception as e: |
| 373 | + if self.sm.error_on_execution: |
| 374 | + self._send_error_execution(trigger_data, e) |
| 375 | + else: |
| 376 | + raise |
333 | 377 |
|
334 | 378 | if len(result) == 0: |
335 | 379 | result = None |
@@ -366,8 +410,12 @@ def _get_args_kwargs( |
366 | 410 | def _conditions_match(self, transition: Transition, trigger_data: TriggerData): |
367 | 411 | args, kwargs = self._get_args_kwargs(transition, trigger_data) |
368 | 412 |
|
369 | | - self.sm._callbacks.call(transition.validators.key, *args, **kwargs) |
370 | | - return self.sm._callbacks.all(transition.cond.key, *args, **kwargs) |
| 413 | + self.sm._callbacks.call( |
| 414 | + transition.validators.key, *args, on_error=self._on_error_execution, **kwargs |
| 415 | + ) |
| 416 | + return self.sm._callbacks.all( |
| 417 | + transition.cond.key, *args, on_error=self._on_error_execution, **kwargs |
| 418 | + ) |
371 | 419 |
|
372 | 420 | def _exit_states( |
373 | 421 | self, enabled_transitions: List[Transition], trigger_data: TriggerData |
@@ -405,9 +453,11 @@ def _exit_states( |
405 | 453 | for info in ordered_states: |
406 | 454 | args, kwargs = self._get_args_kwargs(info.transition, trigger_data) |
407 | 455 |
|
408 | | - # Execute `onexit` handlers |
| 456 | + # Execute `onexit` handlers — same per-block error isolation as onentry. |
409 | 457 | if info.state is not None: # TODO: and not info.transition.internal: |
410 | | - self.sm._callbacks.call(info.state.exit.key, *args, **kwargs) |
| 458 | + self.sm._callbacks.call( |
| 459 | + info.state.exit.key, *args, on_error=self._on_error_execution, **kwargs |
| 460 | + ) |
411 | 461 |
|
412 | 462 | # TODO: Cancel invocations |
413 | 463 | # for invocation in state.invoke: |
@@ -503,8 +553,11 @@ def _enter_states( # noqa: C901 |
503 | 553 | # self.initialize_data_model(state) |
504 | 554 | # state.is_first_entry = False |
505 | 555 |
|
506 | | - # Execute `onentry` handlers |
507 | | - on_entry_result = self.sm._callbacks.call(target.enter.key, *args, **kwargs) |
| 556 | + # Execute `onentry` handlers — each handler is a separate block per |
| 557 | + # SCXML spec: errors in one block MUST NOT affect other blocks. |
| 558 | + on_entry_result = self.sm._callbacks.call( |
| 559 | + target.enter.key, *args, on_error=self._on_error_execution, **kwargs |
| 560 | + ) |
508 | 561 |
|
509 | 562 | # Handle default initial states |
510 | 563 | if target.id in {t.state.id for t in states_for_default_entry if t.state}: |
|
0 commit comments