Preventing PyQt to silence exceptions occurring in slots
Asked Answered
D

3

28

As far as I can see, if an exception occurs in a slot under PyQt, the exception is printed to screen, but not bubbled. This creates a problem in my testing strategy, because if an exception occurs in a slot, I will not see the test fail.

Here is an example:

import sys
from PyQt4 import QtGui, QtCore

class Test(QtGui.QPushButton):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.setText("hello")
        self.connect(self, QtCore.SIGNAL("clicked()"), self.buttonClicked)

    def buttonClicked(self):
        print "clicked"
        raise Exception("wow")

app=QtGui.QApplication(sys.argv)
t=Test()
t.show()
try:
    app.exec_()
except:
    print "exiting"

Note how the exception never quits the program.

Is there a way to work around this problem?

Dunleavy answered 11/9, 2013 at 12:13 Comment(2)
Can you explain this issue a little more? Slots are just designated listeners that call other code. The code that is called when thoriwing errors can be handled like any other.Washburn
@Lego: yes, but if the error is propagated up and leaves the slot, that error is silenced. I'll write an example tonightDunleavy
K
24

Can create a decorator that wraps PyQt' new signal/slot decorators and provides exception handling for all slots. Can also override QApplication::notify to catch uncaught C++ exceptions.

import sys
import traceback
import types
from functools import wraps
from PyQt4 import QtGui, QtCore

def MyPyQtSlot(*args):
    if len(args) == 0 or isinstance(args[0], types.FunctionType):
        args = []
    @QtCore.pyqtSlot(*args)
    def slotdecorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                func(*args)
            except:
                print "Uncaught Exception in slot"
                traceback.print_exc()
        return wrapper

    return slotdecorator

class Test(QtGui.QPushButton):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.setText("hello")
        self.clicked.connect(self.buttonClicked)

    @MyPyQtSlot("bool")
    def buttonClicked(self, checked):
        print "clicked"
        raise Exception("wow")

class MyApp(QtGui.QApplication):
    def notify(self, obj, event):
        isex = False
        try:
            return QtGui.QApplication.notify(self, obj, event)
        except Exception:
            isex = True
            print "Unexpected Error"
            print traceback.format_exception(*sys.exc_info())
            return False
        finally:
            if isex:
                self.quit()

app = MyApp(sys.argv)

t=Test()
t.show()
try:
    app.exec_()
except:
    print "exiting"
Kaohsiung answered 25/9, 2013 at 21:41 Comment(3)
Also, PyQt catches python exceptions and writes to standard error. If you make your application exit on all of these, it might appear that the app is crashing. In the exception handlers, you could in turn trigger a general signal to perform some logging or dialog.Kaohsiung
Also, to exit the app with a non 0 return value, call QtCore.QCoreApplication.exit(1) from your exception handler.Kaohsiung
This answer makes sense only in edge-case applications for which setting sys.excepthook is infeasible. Since setting sys.excepthook is usually feasible, aukaost's solution is almost certainly what everyone should be doing instead: install a global exception hook by assigning the desired callable to sys.excepthook.Tarrance
I
17

You could exit the application with a non-zero return code to indicate that an exception has occurred.
You can catch all exception by installing a global exception hook. I added an example below, but you probably will want to adjust it to your needs.

import sys
from PyQt4 import QtGui, QtCore

class Test(QtGui.QPushButton):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.setText("hello")
        self.connect(self, QtCore.SIGNAL("clicked()"), self.buttonClicked)

    def buttonClicked(self):
        print "clicked"
        raise Exception("wow")

sys._excepthook = sys.excepthook
def exception_hook(exctype, value, traceback):
    sys._excepthook(exctype, value, traceback)
    sys.exit(1)
sys.excepthook = exception_hook

app=QtGui.QApplication(sys.argv)
t=Test()
t.show()
try:
    app.exec_()
except:
    print "exiting"
Interdiction answered 24/9, 2013 at 11:15 Comment(3)
forgot to add. I can't override excepthook... because of reasons.Dunleavy
it's... complicated. :(Dunleavy
This is the sane solution. Demonstrating how to preserve and defer to the standard sys.excepthook implementation is especially helpful.Tarrance
E
3

When running in an IPython console, overriding sys.excepthook does not work because IPython actively overwrites it again when a cell is excecuted.

This is why jlujans solution see above seems very elegant to me.

What I realised is that you can add some nice keyword arguments to the decorator function for customizing the type of exception to catch and also for emitting a pyqtSignal when an exception occurs in a slot. This example runs with PyQt5:

import sys
import traceback
import types
from functools import wraps
from PyQt5.QtCore import pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import QPushButton, QWidget, QApplication, QMessageBox

def pyqtCatchExceptionSlot(*args, catch=Exception, on_exception_emit=None):
    """This is a decorator for pyqtSlots where an exception
    in user code is caught, printed and a optional pyqtSignal with
    signature pyqtSignal(Exception, str) is emitted when that happens.

    Arguments:
    *args:  any valid types for the pyqtSlot
    catch:  Type of the exception to catch, defaults to any exception
    on_exception_emit:  name of a pyqtSignal to be emitted
    """
    if len(args) == 0 or isinstance(args[0], types.FunctionType):
        args = []
    @pyqtSlot(*args)
    def slotdecorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                func(*args)
            except catch as e:
                print(f"In pyqtSlot: {wrapper.__name__}:\n"
                      f"Caught exception: {e.__repr__()}")
                if on_exception_emit is not None:
                    # args[0] is instance of bound signal
                    pyqt_signal = getattr(args[0], on_exception_emit)
                    pyqt_signal.emit(e, wrapper.__name__)
        return wrapper
    return slotdecorator


class Test(QPushButton):
    exceptionOccurred = pyqtSignal(Exception, str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setText("hello")
        self.clicked.connect(self.buttonClicked)
        self.exceptionOccurred.connect(self.on_exceptionOccurred)

    @pyqtSlot(Exception, str)
    def on_exceptionOccurred(self, exception, slot_name):
        QMessageBox.critical(self, "Uncaught exception in pyqtSlot!",
                             f"In pyqtSlot: {slot_name}:\n"
                             f"Caught exception: {exception.__repr__()}")

    @pyqtCatchExceptionSlot("bool", on_exception_emit="exceptionOccurred")
    def buttonClicked(self, checked):
        print("clicked")
        raise Exception("wow")

class MyApp(QApplication):
    def notify(self, obj, event):
        isex = False
        try:
            return QApplication.notify(self, obj, event)
        except Exception:
            isex = True
            print("Unexpected Error")
            print(traceback.format_exception(*sys.exc_info()))
            return False
        finally:
            if isex:
                self.quit()

app = MyApp(sys.argv)

t=Test()
t.show()

# Some boilerplate in case this is run from an IPython shell
try:
    from IPython import get_ipython
    ipy_inst = get_ipython()
    if ipy_inst is None:
        app.exec_()
    else:
        ipy_inst.run_line_magic("gui", "qt5")
except ImportError:
    app.exec_()

What I found also works (but seems like no obvious or clean solution) is monkey-patching the sys.excepthook /inside/ the pqyt event handler which I found in another thread posting:

"""Monkey-patch sys.excepthook /inside/ a PyQt event, e.g. for handling
exceptions occuring in pyqtSlots.
"""
import sys
from traceback import format_exception
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QMessageBox

def new_except_hook(etype, evalue, tb):
    QMessageBox.information(
        None, "Error", "".join(format_exception(etype, evalue, tb)))

def patch_excepthook():
    sys.excepthook = new_except_hook

TIMER = QTimer()
TIMER.setSingleShot(True)
TIMER.timeout.connect(patch_excepthook)
TIMER.start()
Emergency answered 18/7, 2019 at 0:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.