Preventing PyQt to silence exceptions occurring in slots

前端 未结 3 1820
说谎
说谎 2020-11-29 02:59

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

相关标签:
3条回答
  • 2020-11-29 03:21

    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"
    
    0 讨论(0)
  • 2020-11-29 03:33

    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"
    
    0 讨论(0)
  • 2020-11-29 03:34

    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()
    
    0 讨论(0)
提交回复
热议问题