Source code for radioviz.services.exception_handler

#  Copyright 2025–2026 European Union
#  Author: Bulgheroni Antonio (antonio.bulgheroni@ec.europa.eu)
#  SPDX-License-Identifier: EUPL-1.2
"""
Exception handling utilities for Qt applications.

This module provides functions for handling exceptions in a user-friendly way
by displaying them in dialog boxes rather than crashing the application or
only logging to console.

Thanks to the ThreadExceptionHandler, exceptions originating from worker threads are transferred to the main thread
via the thread-safe Qt messaging system and finally handled in the main thread.

In order for this to work, the worker thread must be structured in this way:

.. code-block:: python

    class ExceptionTestThread(QThread):
        def run(self):
            try:
                print('about to raise')
                raise Exception('Test exception from thread')
            except Exception:
                print('caught in thread')
                # Get the thread exception handler and pass the exception info
                from radioviz.exception_handler import (
                    get_thread_exception_handler,
                )

                handler = get_thread_exception_handler()
                handler.handle_exception(*sys.exc_info())
"""

from __future__ import annotations

import sys
import traceback
from types import TracebackType
from typing import Any, Callable, Optional

from PySide6.QtCore import QObject, Qt, Signal
from PySide6.QtWidgets import QMessageBox


[docs] class ThreadExceptionHandler(QObject): """ A handler for exceptions that occur in threads other than the main thread. This class provides a way to safely handle exceptions that occur in QThreads by passing them to the main thread where they can be properly displayed in a QMessageBox. """ exception_caught = Signal(object, object, object) def __init__(self) -> None: super().__init__() # info about QueuedConnection # https://doc.qt.io/qtforpython-6/PySide6/QtCore/Qt.html#PySide6.QtCore.Qt.ConnectionType self.exception_caught.connect(self.show_exception_dialog, Qt.ConnectionType.QueuedConnection)
[docs] def handle_exception( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_traceback: TracebackType | None, ) -> None: """ Handle an exception by emitting a signal to the main thread. :param exc_type: Type of the exception :type exc_type: Type[BaseException] | None :param exc_value: The exception instance :type exc_value: Exception :param exc_traceback: Traceback object :type exc_traceback: TracebackType | None """ # Emit signal to be handled in main thread self.exception_caught.emit(exc_type, exc_value, exc_traceback)
[docs] def show_exception_dialog( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, exc_traceback: TracebackType | None, ) -> None: """ Display an exception in a message box dialog (called in main thread). :param exc_type: Type of the exception :type exc_type: Type[BaseException] | None :param exc_value: The exception instance :type exc_value: Exception :param exc_traceback: Traceback object :type exc_traceback: TracebackType | None """ show_exception_dialog(exc_type, exc_value, exc_traceback)
[docs] def show_exception_dialog( exception_type: type[BaseException] | None, exception_value: BaseException | None, exception_traceback: TracebackType | None, ) -> None: """ Display an exception in a message box dialog. This function creates and displays a dialog box showing details of an exception that occurred during program execution. :param exception_type: Type of the exception :type exception_type: Type[BaseException] | None :param exception_value: The exception instance :type exception_value: Exception :param exception_traceback: Traceback object :type exception_traceback: TracebackType | None """ # Format the traceback error_msg = ''.join(traceback.format_exception(exception_type, exception_value, exception_traceback)) # Create and show the error dialog error_box = QMessageBox() error_box.setIcon(QMessageBox.Icon.Critical) error_box.setWindowTitle('Error') error_box.setText(f'An error occurred: {str(exception_value)}') error_box.setDetailedText(error_msg) error_box.setStandardButtons(QMessageBox.StandardButton.Ok) error_box.exec() # Also print to stderr for logging purposes print(error_msg, file=sys.stderr)
#: The global instance of the thread exception handler _thread_exception_handler: Optional[ThreadExceptionHandler] = None
[docs] def get_thread_exception_handler() -> ThreadExceptionHandler: """ Get or create the global thread exception handler. :return: The global thread exception handler instance :rtype: ThreadExceptionHandler """ global _thread_exception_handler if _thread_exception_handler is None: _thread_exception_handler = ThreadExceptionHandler() return _thread_exception_handler
[docs] def install_exception_handler() -> Callable[[type[BaseException], BaseException, TracebackType | None], Any]: """ Install a global exception handler to show errors in a dialog box. This function replaces the default Python exception handler with the custom implementation that displays errors in a graphical dialog. It should be called after QApplication is created. :return: The original exception handler function :rtype: callable """ # Store the original excepthook original_excepthook = sys.excepthook # Set our custom exception handler sys.excepthook = show_exception_dialog # Initialize the thread exception handler get_thread_exception_handler() return original_excepthook