Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Understanding PyQt and raising exceptions #25

Closed
datalyze-solutions opened this issue Oct 29, 2014 · 7 comments
Closed

Understanding PyQt and raising exceptions #25

datalyze-solutions opened this issue Oct 29, 2014 · 7 comments

Comments

@datalyze-solutions
Copy link

Hi, I have a few problems understanding the behavior of PyQt and exception handling. Why is no exception raised when zeroDivide() is called by the button click? Is there another solution except to mark the test as expected to fail? Why is the test test_buttonClick2() started two times and test_testFail() just once?

    import pytest
    from pytestqt.qt_compat import QtGui, QtCore

    class TestWidget(QtGui.QWidget):

        def __init__(self, parent=None):
            super(TestWidget, self).__init__(parent)

            self.horizontalLayout = QtGui.QHBoxLayout(self)
            self.button = QtGui.QPushButton("raise exception")
            self.horizontalLayout.addWidget(self.button)
            self.button.clicked.connect(self.zeroDivide)

        def zeroDivide(self):
            0 / 0

    class TestClass(object):

        def setup_class(self):
            self.widget = TestWidget()

        def teardown_class(self):
            pass

        def test_zeroDivide(self):
            with pytest.raises(ZeroDivisionError):
                self.widget.zeroDivide()

        def test_zeroDivide2(self, qtbot):
            qtbot.addWidget(self.widget)
            with pytest.raises(ZeroDivisionError):
                qtbot.mouseClick(self.widget.button, QtCore.Qt.LeftButton)

        @pytest.mark.xfail
        def test_testFail(self):
            assert False

        @pytest.mark.xfail
        def test_buttonClick2(self, qtbot):
            qtbot.addWidget(self.widget)
            qtbot.mouseClick(self.widget.button, QtCore.Qt.LeftButton)
@nicoddemus
Copy link
Member

Hi,

Unfortunately both PySide and PyQt don't let exceptions raised from slots to propagate back to the caller. QtBot tries to do its best here by raising an error during test tear down if it detects that an exception occurred during a test: that's why you see two errors originating from test_zeroDivide, one from the fact that qtbot.mouseClick does not raise an error (because PySide/PyQt eats the error) and another during tear down raised by QtBot because it detected the exception being raised.

You can see some other workarounds here which might be a good fit for you particular application.

Hope that helps,
Cheers! 😄

@datalyze-solutions
Copy link
Author

Thanks for the quick answer!
I fiddled around a bit to overwrite the sys.excepthook and it worked:

    from PyQt4 import QtGui
    from PyQt4 import QtCore
    from PyQt4.QtCore import Qt
    import sys

    class ExceptionHandler(QtCore.QObject):

        errorSignal = QtCore.pyqtSignal()

        def __init__(self):
            super(ExceptionHandler, self).__init__()

        def handler(self, exctype, value, traceback):
            self.errorSignal.emit()
            sys._excepthook(exctype, value, traceback)

    exceptionHandler = ExceptionHandler()
    sys._excepthook = sys.excepthook
    sys.excepthook = exceptionHandler.handler

    def something():
        print "ERROR ERROR ERROR"

    class Test(QtGui.QPushButton):

        def __init__(self, parent=None):
            QtGui.QWidget.__init__(self, parent)
            self.setText("hello")
            self.resize(300, 400)
            self.clicked.connect(self.division)

        def division(self):
            0 / 0

    if __name__ == '__main__':
        app=QtGui.QApplication(sys.argv)
        widget = Test()
        widget.show()

        exceptionHandler.errorSignal.connect(something)

        app.exec_()

My ExceptionHandler is a QObject cause I hoped to use qtbots wait signal and wait for errorSignal fired. Something like:

    def test_zero(self, qtbot):
        qtbot.addWidget(self.widget)
        self.widget.clicked.connect(self.widget.zeroDivide)
        with qtbot.waitSignal(exceptionHandler.testSignal, timeout=1000) as blocker:
            qtbot.mouseClick(self.widget, QtCore.Qt.LeftButton)

        assert blocker.signal_triggered

As far as I understand it qtbot (or pytest?!) overwrites the sys.excepthook. So my own rewrite doesnt work anymore. Any suggestions?

@nicoddemus
Copy link
Member

Hi,

qtbot overwrites sys.excepthook during each test invocation. Here's the relevant code:

# pytestqt.plugin.py
@contextmanager
def capture_exceptions():
    """
    Context manager that captures exceptions that happen insides its context,
    and returns them as a list of (type, value, traceback) after the
    context ends.
    """
    result = []

    def hook(type_, value, tback):
        result.append((type_, value, tback))
        sys.__excepthook__(type_, value, tback)

    sys.excepthook = hook
    try:
        yield result
    finally:
        sys.excepthook = sys.__excepthook__

@pytest.yield_fixture
def qtbot(qapp):
    """
    Fixture used to create a QtBot instance for using during testing.

    Make sure to call addWidget for each top-level widget you create to ensure
    that they are properly closed after the test ends.
    """
    result = QtBot(qapp)
    with capture_exceptions() as exceptions:
        yield result

    if exceptions:
        pytest.fail(format_captured_exceptions(exceptions))

    result._close()

As you can see, it overwrites the hook so it can check if exceptions happened during testing.

Do you plan to overwrite the sys.excepthook in your code? If so, perhaps we can introduce a new pytest.ini flag to prevent qtbot from overwriting sys.excepthook. Would that work out for you?

hint: you can add syntax highlighting to your sample code when writing on GitHub by using fenced code blocks and adding the language in the first line like this:

```python
def foo(self):
```

Took the liberty and updated your post above. 😄

@datalyze-solutions
Copy link
Author

Hi,

thanks for the hint! I keep it in mind for the next time 😄

The ini idea sounds good. I'am not sure if it will work for me, but it sounds good 😄 (I'am not that experienced with pytest nor testing at all...). If it's not that complicate to implement I opt for it.

Cheers!

@nicoddemus
Copy link
Member

Can you manually comment out the lines in pytest-qt that capture the exception:

@pytest.yield_fixture
def qtbot(qapp):
    result = QtBot(qapp)
    exceptions = []
    #with capture_exceptions() as exceptions:
    yield result

    if exceptions:
        pytest.fail(format_captured_exceptions(exceptions))

    result._close()

To see if that fits well with your code? If that's the case, I can implement the ini option quite easily.

@datalyze-solutions
Copy link
Author

I changed the code in plugin.py to:

@pytest.yield_fixture
def qtbot(request):
    #"""
    #Fixture used to create a QtBot instance for using during testing. 

    #Make sure to call addWidget for each top-level widget you create to ensure
    #that they are properly closed after the test ends.
    #"""
    result = QtBot(request.config.qt_app_instance)
    yield result
    #with capture_exceptions() as exceptions:
        #yield result

    #if exceptions:
        #pytest.fail(format_captured_exceptions(exceptions))

    result._close()

The following test runs like a charm:

import pytest
import pytestqt

from PyQt4 import QtGui
from PyQt4 import QtCore
from PyQt4.QtCore import Qt
import sys

class ExceptionHandler(QtCore.QObject):

    errorSignal = QtCore.pyqtSignal()
    silentSignal = QtCore.pyqtSignal()

    def __init__(self):
        super(ExceptionHandler, self).__init__()

    def handler(self, exctype, value, traceback):
        self.errorSignal.emit()
        print "ERROR CAPTURED"
        sys._excepthook(exctype, value, traceback)

exceptionHandler = ExceptionHandler()
sys._excepthook = sys.excepthook
sys.excepthook = exceptionHandler.handler

class Test(QtGui.QPushButton):

    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)
        self.setText("hello")
        self.resize(300, 400)
        self.clicked.connect(self.division)

    def division(self):
        0 / 0

class TestClass(object):

    def setup_class(self):
        self.widget = Test()

    def teardown_class(self):
        pass

    def test_zero(self):
        with pytest.raises(ZeroDivisionError):
            self.widget.division()    

    def test_division(self, qtbot):
        qtbot.addWidget(self.widget)
        with qtbot.waitSignal(exceptionHandler.errorSignal, timeout=1000) as blocker:
            qtbot.mouseClick(self.widget, QtCore.Qt.LeftButton)

        assert blocker.signal_triggered

    def test_silentSignal(self, qtbot):
        qtbot.addWidget(self.widget)
        with qtbot.waitSignal(exceptionHandler.silentSignal, timeout=1000) as blocker:
            qtbot.mouseClick(self.widget, QtCore.Qt.LeftButton)

        assert not blocker.signal_triggered

@nicoddemus
Copy link
Member

Excellent! 😄

Thanks for bringing this up, I will implement this in #26.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant