Reputation: 647
Have a look at the following MWE.
import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.button = QPushButton('Bham!')
self.setCentralWidget(self.button)
self.button.clicked.connect(self.btnClicked)
def btnClicked(self):
print(sys.excepthook)
raise Exception
#import traceback
#sys.excepthook = traceback.print_exception
if __name__ == '__main__':
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec_()
I have a number of questions. I don't know if they are all related (I guess so), so forgive me if they are not.
When I run the above code from the terminal, all is fine. The program runs, if I click the button it prints the traceback and dies. If I run it inside an IDE (I tested Spyder and PyCharm), the traceback is not displayed. Any idea why? Essentially the same question was raised in other posts also on SO, here and here. Please don't mark this as a duplicate of either of those; please read on.
By adding the commented lines, the traceback is again displayed properly. However, they also have the nasty side effect that the app does no longer terminate on unhandled exceptions! I have no idea why this happens, as AFAIK excepthook
only prints the traceback, it cannot prevent the program from exiting. At the moment it is called, it's too late for rescue.
Also, I don't understand how Qt comes into play here, as exceptions that are not thrown inside a slot still crash the app as I would expect. No matter if I change excepthook
or not, PyQt does not seem to override it as well (at least the print
seems to suggest so).
FYI, I am using Python 3.5 with PyQt 5.6, and I am aware of the changes in the exception handling introduced in PyQt 5.5. If those are indeed the cause for the behaviour above, I would be glad hear some more detailed explanations.
Upvotes: 8
Views: 3749
Reputation: 895
Whilst @the-compiler's answer is correct in explaining why it happens, I thought I might provide a workaround if you'd like these exceptions to be raised in a more pythony way.
I decorate any slots with this decorator, which catches any exceptions in the slot and saves them to a a global variable:
exc_info = None
def pycrash(func):
"""Decorator that quits the qt mainloop and stores sys.exc_info. We will then
raise it outside the qt mainloop, this is a cleaner crash than Qt just aborting as
it does if Python raises an exception during a callback."""
def f(*args, **kwargs):
global exc_info
try:
return func(*args, **kwargs)
except:
if exc_info is None # newer exceptions don't replace the first one
exc_info = sys.exc_info()
qapplication.exit()
return f
Then just after my QApplication
's exec()
, I check the global variable and raise if there's anything there:
qapplication.exec_()
if exc_info is not None:
type, value, traceback = exc_info
raise value.with_traceback(traceback)
This is not ideal because quitting the mainloop doesn't stop other slots higher in the stack from still completing, and if the failed slot affects them, they might see some unexpected state. But IMHO it's still much better than PyQt just aborting with no cleanup.
Upvotes: 0
Reputation: 11929
When an exception happens inside a Qt slot, it's C++ which called into your Python code. Since Qt/C++ know nothing about Python exceptions, you only have two possibilities:
The reason Python still doesn't terminate is probably because it can't, as it's inside some C++ code. The reason your IDE isn't showing the stack is probably because it doesn't deal with the abort()
correctly - I'd suggest opening a bug against the IDE for that.
Upvotes: 5