Skip to content

Commit ef9a51f

Browse files
committed
Merge pull request #28 from nicoddemus/issue-26
Option for disabling automatic exception capture
2 parents c3d0e1a + ddcc7e7 commit ef9a51f

File tree

3 files changed

+125
-9
lines changed

3 files changed

+125
-9
lines changed

docs/index.rst

+62
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,68 @@ ensuring the results are correct::
155155
assert_application_results(app)
156156

157157

158+
Exceptions in virtual methods
159+
=============================
160+
161+
It is common in Qt programming to override virtual C++ methods to customize
162+
behavior, like listening for mouse events, implement drawing routines, etc.
163+
164+
Fortunately, both ``PyQt`` and ``PySide`` support overriding this virtual methods
165+
naturally in your python code::
166+
167+
class MyWidget(QWidget):
168+
169+
# mouseReleaseEvent
170+
def mouseReleaseEvent(self, ev):
171+
print('mouse released at: %s' % ev.pos())
172+
173+
This works fine, but if python code in Qt virtual methods raise an exception
174+
``PyQt`` and ``PySide`` will just print the exception traceback to standard
175+
error, since this method is called deep within Qt's even loop handling and
176+
exceptions are not allowed at that point.
177+
178+
This might be surprising for python users which are used to exceptions
179+
being raised at the calling point: for example, the following code will just
180+
print a stack trace without raising any exception::
181+
182+
class MyWidget(QWidget):
183+
184+
def mouseReleaseEvent(self, ev):
185+
raise RuntimeError('unexpected error')
186+
187+
w = MyWidget()
188+
QTest.mouseClick(w, QtCore.Qt.LeftButton)
189+
190+
191+
To make testing Qt code less surprising, ``pytest-qt`` automatically
192+
installs an exception hook which captures errors and fails tests when exceptions
193+
are raised inside virtual methods, like this::
194+
195+
E Failed: Qt exceptions in virtual methods:
196+
E ________________________________________________________________________________
197+
E File "x:\pytest-qt\pytestqt\_tests\test_exceptions.py", line 14, in event
198+
E raise RuntimeError('unexpected error')
199+
E
200+
E RuntimeError: unexpected error
201+
202+
203+
Disabling the automatic exception hook
204+
--------------------------------------
205+
206+
You can disable the automatic exception hook on individual tests by using a
207+
``qt_no_exception_capture`` marker::
208+
209+
@pytest.mark.qt_no_exception_capture
210+
def test_buttons(qtbot):
211+
...
212+
213+
Or even disable it for your entire project in your ``pytest.ini`` file::
214+
215+
[pytest]
216+
qt_no_exception_capture = 1
217+
218+
This might be desirable if you plan to install a custom exception hook.
219+
158220
QtBot
159221
=====
160222

pytestqt/_tests/test_exceptions.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import textwrap
21
import pytest
32
import sys
43
from pytestqt.plugin import capture_exceptions, format_captured_exceptions
54
from pytestqt.qt_compat import QtGui, Qt, QtCore
65

76

7+
pytest_plugins = 'pytester'
8+
9+
810
class Receiver(QtCore.QObject):
911
"""
1012
Dummy QObject subclass that raises an error on receiving events if
@@ -45,4 +47,40 @@ def test_format_captured_exceptions():
4547
lines = obtained_text.splitlines()
4648

4749
assert 'Qt exceptions in virtual methods:' in lines
48-
assert 'ValueError: errors were made' in lines
50+
assert 'ValueError: errors were made' in lines
51+
52+
53+
@pytest.mark.parametrize('no_capture_by_marker', [True, False])
54+
def test_no_capture(testdir, no_capture_by_marker):
55+
"""
56+
Make sure options that disable exception capture are working (either marker
57+
or ini configuration value).
58+
:type testdir: TmpTestdir
59+
"""
60+
if no_capture_by_marker:
61+
marker_code = '@pytest.mark.qt_no_exception_capture'
62+
else:
63+
marker_code = ''
64+
testdir.makeini('''
65+
[pytest]
66+
qt_no_exception_capture = 1
67+
''')
68+
testdir.makepyfile('''
69+
import pytest
70+
from pytestqt.qt_compat import QtGui, QtCore
71+
72+
class MyWidget(QtGui.QWidget):
73+
74+
def mouseReleaseEvent(self, ev):
75+
raise RuntimeError
76+
77+
{marker_code}
78+
def test_widget(qtbot):
79+
w = MyWidget()
80+
qtbot.addWidget(w)
81+
qtbot.mouseClick(w, QtCore.Qt.LeftButton)
82+
'''.format(marker_code=marker_code))
83+
result = testdir.runpytest('-s')
84+
# when it fails, it fails with "1 passed, 1 error in", so ensure
85+
# it is passing without errors
86+
result.stdout.fnmatch_lines('*1 passed in*')

pytestqt/plugin.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class QtBot(object):
7676
**Raw QTest API**
7777
7878
Methods below provide very low level functions, as sending a single mouse click or a key event.
79-
Thos methods are just forwarded directly to the `QTest API`_. Consult the documentation for more
79+
Those methods are just forwarded directly to the `QTest API`_. Consult the documentation for more
8080
information.
8181
8282
---
@@ -380,18 +380,34 @@ def qapp():
380380

381381

382382
@pytest.yield_fixture
383-
def qtbot(qapp):
383+
def qtbot(qapp, request):
384384
"""
385385
Fixture used to create a QtBot instance for using during testing.
386386
387387
Make sure to call addWidget for each top-level widget you create to ensure
388388
that they are properly closed after the test ends.
389389
"""
390390
result = QtBot(qapp)
391-
with capture_exceptions() as exceptions:
392-
yield result
393-
394-
if exceptions:
395-
pytest.fail(format_captured_exceptions(exceptions))
391+
no_capture = request.node.get_marker('qt_no_exception_capture') or \
392+
request.config.getini('qt_no_exception_capture')
393+
if no_capture:
394+
yield result # pragma: no cover
395+
else:
396+
with capture_exceptions() as exceptions:
397+
yield result
398+
if exceptions:
399+
pytest.fail(format_captured_exceptions(exceptions))
396400

397401
result._close()
402+
403+
404+
def pytest_addoption(parser):
405+
parser.addini('qt_no_exception_capture',
406+
'disable automatic exception capture')
407+
408+
409+
def pytest_configure(config):
410+
config.addinivalue_line(
411+
'markers',
412+
"qt_no_exception_capture: Disables pytest-qt's automatic exception "
413+
'capture for just one test item.')

0 commit comments

Comments
 (0)