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?
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
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"
Method 2
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"
Method 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()
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0