Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exception handled surprisingly in Pyside slots

Problem: When exceptions are raised in slots, invoked by signals, they do not seem to propagate as usual through Pythons call stack. In the example code below invoking:

  • on_raise_without_signal(): Will handle the exception as expected.
  • on_raise_with_signal(): Will print the exception and then unexpectedly print the success message from the else block.

Question: What is the reason behind the exception being handled surprisingly when raised in a slot? Is it some implementation detail/limitation of the PySide Qt wrapping of signals/slots? Is there something to read about in the docs?

PS: I initially came across that topic when I got surprising results upon using try/except/else/finally when implementing a QAbstractTableModels virtual methods insertRows() and removeRows().


# -*- coding: utf-8 -*-
"""Testing exception handling in PySide slots."""
from __future__ import unicode_literals, print_function, division

import logging
import sys

from PySide import QtCore
from PySide import QtGui


logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


class ExceptionTestWidget(QtGui.QWidget):

    raise_exception = QtCore.Signal()

    def __init__(self, *args, **kwargs):
        super(ExceptionTestWidget, self).__init__(*args, **kwargs)

        self.raise_exception.connect(self.slot_raise_exception)

        layout = QtGui.QVBoxLayout()
        self.setLayout(layout)

        # button to invoke handler that handles raised exception as expected
        btn_raise_without_signal = QtGui.QPushButton("Raise without signal")
        btn_raise_without_signal.clicked.connect(self.on_raise_without_signal)
        layout.addWidget(btn_raise_without_signal)

        # button to invoke handler that handles raised exception via signal unexpectedly
        btn_raise_with_signal = QtGui.QPushButton("Raise with signal")
        btn_raise_with_signal.clicked.connect(self.on_raise_with_signal)
        layout.addWidget(btn_raise_with_signal)

    def slot_raise_exception(self):
        raise ValueError("ValueError on purpose")

    def on_raise_without_signal(self):
        """Call function that raises exception directly."""
        try:
            self.slot_raise_exception()
        except ValueError as exception_instance:
            logger.error("{}".format(exception_instance))
        else:
            logger.info("on_raise_without_signal() executed successfully")

    def on_raise_with_signal(self):
        """Call slot that raises exception via signal."""
        try:
            self.raise_exception.emit()
        except ValueError as exception_instance:
            logger.error("{}".format(exception_instance))
        else:
            logger.info("on_raise_with_signal() executed successfully")


if (__name__ == "__main__"):
    application = QtGui.QApplication(sys.argv)

    widget = ExceptionTestWidget()
    widget.show()

    sys.exit(application.exec_())
like image 420
timmwagener Avatar asked Aug 20 '17 22:08

timmwagener


1 Answers

As you've already noted in your question, the real issue here is the treatment of unhandled exceptions raised in python code executed from C++. So this is not only about signals: it also affects reimplemented virtual methods as well.

In PySide, PyQt4, and all PyQt5 versions up to 5.5, the default behaviour is to automatically catch the error on the C++ side and dump a traceback to stderr. Normally, a python script would also automatically terminate after this. But that is not what happens here. Instead, the PySide/PyQt script just carries on regardless, and many people quite rightly regard this as a bug (or at least a misfeature). In PyQt-5.5, this behaviour has now been changed so that qFatal() is also called on the C++ side, and the program will abort like a normal python script would. (I don't know what the current situation is with PySide2, though).

So - what should be done about all this? The best solution for all versions of PySide and PyQt is to install an exception hook - because it will always take precedence over the default behaviour (whatever that may be). Any unhandled exception raised by a signal, virtual method or other python code will firstly invoke sys.excepthook, allowing you to fully customise the behaviour in whatever way you like.

In your example script, this could simply mean adding something like this:

def excepthook(cls, exception, traceback):
    print('calling excepthook...')
    logger.error("{}".format(exception))

sys.excepthook = excepthook

and now the exception raised by on_raise_with_signal can be handled in the same way as all other unhandled exceptions.

Of course, this does imply that best practice for most PySide/PyQt applications is to use largely centralised exception handling. This often includes showing some kind of crash-dialog where the user can report unexpected errors.

like image 115
ekhumoro Avatar answered Nov 14 '22 23:11

ekhumoro