Подтвердить что ты не робот

Предотвращение PyQt для отключения исключений, возникающих в слотах

Насколько я вижу, если в слоте под 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"

Обратите внимание, что исключение никогда не покидает программу.

Есть ли способ обойти эту проблему?

4b9b3361

Ответ 1

Можно создать декоратор, который обертывает новые декодеры сигналов/слотов PyQt и обеспечивает обработку исключений для всех слотов. Также можно переопределить QApplication:: notify, чтобы поймать непонятые исключения С++.

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"

Ответ 2

Вы можете выйти из приложения с ненулевым кодом возврата, чтобы указать, что произошло исключение.
Вы можете поймать все исключения, установив глобальный крюк исключений. Я добавил пример ниже, но вы, вероятно, захотите его подстроить под свои нужды.

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"

Ответ 3

При запуске в консоли IPython переопределение sys.excepthook не работает, потому что IPython снова активно перезаписывает его при освобождении ячейки.

Вот почему решение jlujans, см. Выше, кажется мне очень элегантным.

Я понял, что вы можете добавить несколько хороших аргументов ключевого слова в функцию декоратора для настройки типа исключения, которое нужно перехватить, а также для выдачи 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()