# 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