From 87f251dfe48502fc835322764cef1609138111de Mon Sep 17 00:00:00 2001 From: wakonig_k Date: Thu, 11 Dec 2025 17:22:21 +0100 Subject: [PATCH] feat: pretty print errors on scan history access --- bec_ipython_client/bec_ipython_client/main.py | 46 +++------- bec_lib/bec_lib/alarm_handler.py | 33 +++++++ bec_lib/bec_lib/bec_errors.py | 85 +++++++++++++++++++ bec_lib/bec_lib/messages.py | 1 + bec_lib/bec_lib/scan_history.py | 26 +++++- 5 files changed, 156 insertions(+), 35 deletions(-) diff --git a/bec_ipython_client/bec_ipython_client/main.py b/bec_ipython_client/bec_ipython_client/main.py index 34d373fa9..87cc95378 100644 --- a/bec_ipython_client/bec_ipython_client/main.py +++ b/bec_ipython_client/bec_ipython_client/main.py @@ -24,7 +24,7 @@ from bec_ipython_client.signals import ScanInterruption, SigintHandler from bec_lib import plugin_helper from bec_lib.alarm_handler import AlarmBase -from bec_lib.bec_errors import DeviceConfigError +from bec_lib.bec_errors import BECError, DeviceConfigError from bec_lib.bec_service import parse_cmdline_args from bec_lib.callback_handler import EventType from bec_lib.client import BECClient @@ -207,38 +207,12 @@ def show_last_alarm(self, offset: int = 0): except IndexError: print("No alarm has been raised in this session.") return - - console = Console() - - # --- HEADER --- - header = Text() - header.append("Alarm Raised\n", style="bold red") - header.append(f"Severity: {alarm.severity.name}\n", style="bold") - header.append(f"Type: {alarm.alarm_type}\n", style="bold") - if alarm.alarm.info.device: - header.append(f"Device: {alarm.alarm.info.device}\n", style="bold") - - console.print(Panel(header, title="Alarm Info", border_style="red", expand=False)) - - # --- SHOW SUMMARY - if alarm.alarm.info.compact_error_message: - console.print( - Panel( - Text(alarm.alarm.info.compact_error_message, style="yellow"), - title="Summary", - border_style="yellow", - expand=False, - ) - ) - - # --- SHOW FULL TRACEBACK - tb_str = alarm.alarm.info.error_message - if tb_str: - try: - console.print(tb_str) - except Exception: - # fallback in case msg is not a traceback - console.print(Panel(tb_str, title="Message", border_style="cyan")) + if hasattr(alarm, "print_details"): + alarm.print_details() + return + if hasattr(alarm, "pretty_print"): + alarm.pretty_print() + return def _ip_exception_handler( @@ -253,6 +227,12 @@ def _ip_exception_handler( if issubclass(etype, ValidationError): pretty_print_pydantic_validation_error(evalue) return + if issubclass(etype, BECError): + parent._alarm_history.append((etype, evalue, tb, tb_offset)) + evalue.pretty_print() + print("For more details, use 'bec.show_last_alarm()'") + return + if issubclass(etype, (ScanInterruption, DeviceConfigError)): print(f"\x1b[31m {evalue.__class__.__name__}:\x1b[0m {evalue}") return diff --git a/bec_lib/bec_lib/alarm_handler.py b/bec_lib/bec_lib/alarm_handler.py index 8fa2ff365..5c9683c23 100644 --- a/bec_lib/bec_lib/alarm_handler.py +++ b/bec_lib/bec_lib/alarm_handler.py @@ -81,6 +81,39 @@ def pretty_print(self) -> None: console.print(Panel(body, title=text, border_style="red", expand=True)) + def print_details(self) -> None: + console = Console() + + # --- HEADER --- + header = Text() + header.append("Alarm Raised\n", style="bold red") + header.append(f"Severity: {self.severity.name}\n", style="bold") + header.append(f"Type: {self.alarm_type}\n", style="bold") + if self.alarm.info.device: + header.append(f"Device: {self.alarm.info.device}\n", style="bold") + + console.print(Panel(header, title="Alarm Info", border_style="red", expand=False)) + + # --- SHOW SUMMARY + if self.alarm.info.compact_error_message: + console.print( + Panel( + Text(self.alarm.info.compact_error_message, style="yellow"), + title="Summary", + border_style="yellow", + expand=False, + ) + ) + + # --- SHOW FULL TRACEBACK + tb_str = self.alarm.info.error_message + if tb_str: + try: + console.print(tb_str) + except Exception: + # fallback in case msg is not a traceback + console.print(Panel(tb_str, title="Message", border_style="cyan")) + def __eq__(self, other: object) -> bool: if not isinstance(other, AlarmBase): return False diff --git a/bec_lib/bec_lib/bec_errors.py b/bec_lib/bec_lib/bec_errors.py index 11d47373a..4f9649694 100644 --- a/bec_lib/bec_lib/bec_errors.py +++ b/bec_lib/bec_lib/bec_errors.py @@ -2,6 +2,91 @@ This module contains the custom exceptions used in the BEC library. """ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from rich.console import Console +from rich.panel import Panel +from rich.syntax import Syntax +from rich.text import Text + +if TYPE_CHECKING: # pragma: no cover + from bec_lib import messages + + +class BECError(Exception): + """Base class for all BEC exceptions""" + + def __init__(self, message: str, error_info: messages.ErrorInfo) -> None: + super().__init__(message) + self.error_info = error_info + + def pretty_print(self) -> None: + """ + Use Rich to pretty print the alarm message, + following the same logic used in __str__(). + """ + console = Console() + + msg = self.error_info.compact_error_message or self.error_info.error_message + + text = Text() + text.append(f"{self.error_info.exception_type}", style="bold") + if self.error_info.context: + text.append(f" | {self.error_info.context}", style="bold") + + if self.error_info.device: + text.append(f" | Device {self.error_info.device}\n", style="bold") + text.append("\n") + + # Format message inside a syntax box if it looks like traceback + if "Traceback (most recent call last):" in msg: + body = Syntax(msg.strip(), "python", word_wrap=True) + else: + body = Text(msg.strip()) + + if self.error_info.device: + body.append( + f"\n\nThe error is likely unrelated to BEC. Please check the device '{self.error_info.device}'.", + style="bold", + ) + + console.print(Panel(body, title=text, border_style="red", expand=True)) + + def print_details(self) -> None: + console = Console() + + # --- HEADER --- + header = Text() + header.append("Error Occurred\n", style="bold red") + header.append(f"Type: {self.error_info.exception_type}\n", style="bold") + if self.error_info.context: + header.append(f"Context: {self.error_info.context}\n", style="bold") + if self.error_info.device: + header.append(f"Device: {self.error_info.device}\n", style="bold") + + console.print(Panel(header, title="Error Info", border_style="red", expand=False)) + + # --- SHOW SUMMARY + if self.error_info.compact_error_message: + console.print( + Panel( + Text(self.error_info.compact_error_message), + title="Error Summary", + border_style="red", + expand=False, + ) + ) + + # --- SHOW FULL TRACEBACK + tb_str = self.error_info.error_message + if tb_str: + try: + console.print(tb_str) + except Exception as e: + console.print(f"Error printing traceback: {e}", style="bold red") + class ScanAbortion(Exception): """Scan abortion exception""" diff --git a/bec_lib/bec_lib/messages.py b/bec_lib/bec_lib/messages.py index 66b99d320..4f6cb203c 100644 --- a/bec_lib/bec_lib/messages.py +++ b/bec_lib/bec_lib/messages.py @@ -432,6 +432,7 @@ class ErrorInfo(BaseModel): compact_error_message: str | None exception_type: str device: str | list[str] | None = None + context: str | None = None class DeviceInstructionResponse(BECMessage): diff --git a/bec_lib/bec_lib/scan_history.py b/bec_lib/bec_lib/scan_history.py index f99a2f104..266bc07e8 100644 --- a/bec_lib/bec_lib/scan_history.py +++ b/bec_lib/bec_lib/scan_history.py @@ -6,14 +6,16 @@ import os import threading +import traceback from typing import TYPE_CHECKING +from bec_lib import messages +from bec_lib.bec_errors import BECError from bec_lib.callback_handler import EventType from bec_lib.endpoints import MessageEndpoints from bec_lib.scan_data_container import ScanDataContainer if TYPE_CHECKING: # pragma: no cover - from bec_lib import messages from bec_lib.client import BECClient @@ -143,7 +145,27 @@ def __len__(self) -> int: def __getitem__(self, index: int | slice) -> ScanDataContainer | list[ScanDataContainer]: with self._scan_data_lock: if isinstance(index, int): - target_id = self._scan_ids[index] + try: + target_id = self._scan_ids[index] + except IndexError: + if len(self._scan_ids) == 0: + compact_msg = ( + f"ScanHistory is empty. This may be due to no scans being " + f"run yet or the current user {os.getlogin()} not having access to the data files." + ) + else: + compact_msg = ( + f"Index {index} out of range for ScanHistory of length {len(self)}" + ) + error_info = messages.ErrorInfo( + error_message=traceback.format_exc(), + compact_error_message=compact_msg, + exception_type="IndexError", + context="ScanHistory", + device=None, + ) + raise BECError(compact_msg, error_info) + return self.get_by_scan_id(target_id) if isinstance(index, slice): return [self.get_by_scan_id(scan_id) for scan_id in self._scan_ids[index]]