据我所知,如果在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"
注意异常永远不会退出程序。
有办法解决这个问题吗?
答案 0 :(得分:19)
可以创建一个装饰器来包装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"
答案 1 :(得分:11)
您可以使用非零返回码退出应用程序,以指示发生了异常 您可以通过安装全局异常挂钩来捕获所有异常。 我在下面添加了一个示例,但您可能希望根据需要进行调整。
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"
答案 2 :(得分:0)
在IPython控制台中运行时,覆盖sys.excepthook无效,因为当执行单元格执行时,IPython会再次主动覆盖它。
这就是jlujans解决方案see above在我看来非常优雅的原因。
我意识到的是,您可以向装饰器函数添加一些不错的关键字参数,以自定义异常的类型以捕获,并且还可以在插槽中发生异常时发出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 /在another thread posting中找到的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()