27

据我所知,如果 PyQt 下的插槽中发生异常,异常会打印到屏幕上,但不会冒泡。这在我的测试策略中产生了一个问题,因为如果某个插槽中发生异常,我将不会看到测试失败。

这是一个例子:

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"

请注意异常如何永远不会退出程序。

有没有办法解决这个问题?

4

3 回答 3

24

可以创建一个装饰器来包装 PyQt 的新信号/槽装饰器,并为所有槽提供异常处理。还可以覆盖 QApplication::notify 以捕获未捕获的 C++ 异常。

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"
于 2013-09-25T21:41:36.373 回答
15

您可以使用非零返回码退出应用程序以指示发生了异常。
您可以通过安装全局异常挂钩来捕获所有异常。我在下面添加了一个示例,但您可能希望根据需要对其进行调整。

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"
于 2013-09-24T11:15:02.470 回答
3

在 IPython 控制台中运行时,覆盖 sys.excepthook 不起作用,因为 IPython 在执行单元时会再次主动覆盖它。

这就是为什么上面看到的 jlujans 解决方案对我来说非常优雅。

我意识到,您可以向装饰器函数添加一些不错的关键字参数,以自定义要捕获的异常类型,并在 slot 中发生异常时发出 pyqtSignal。此示例使用 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_()

我发现也有效(但似乎没有明显或干净的解决方案)是猴子修补 sys.excepthook /inside/ 我在另一个帖子发布的 pqyt 事件处理程序:

"""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()
于 2019-07-18T00:47:30.397 回答