Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 13 additions & 33 deletions bec_ipython_client/bec_ipython_client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions bec_lib/bec_lib/alarm_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
85 changes: 85 additions & 0 deletions bec_lib/bec_lib/bec_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
1 change: 1 addition & 0 deletions bec_lib/bec_lib/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
26 changes: 24 additions & 2 deletions bec_lib/bec_lib/scan_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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]]
Expand Down
Loading