Skip to content

Commit 25c9071

Browse files
committed
Add note about PySide6 exception capture and fix tests
PySide6 does its own exception capture during events, re-raising them when control gets back to Python. This feature causes our own exception capturing mechanism to not work, but it is not needed anyway, because the exception re-raising done by PySide6 contains a better traceback than it is possible for pytest-qt to provide, given it can re-raise at the exact point where control was given back to Python. Besides noting this in the documentation, also accomodate the tests.
1 parent 6b4639f commit 25c9071

File tree

2 files changed

+47
-14
lines changed

2 files changed

+47
-14
lines changed

docs/virtual_methods.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ Exceptions in virtual methods
33

44
.. versionadded:: 1.1
55

6+
.. note::
7+
8+
``PySide6 6.5.2+`` automatically captures exceptions that happen during the Qt event loop and
9+
re-raises them when control is moved back to Python, so the functionality described here
10+
does not work with ``PySide6`` (nor is necessary).
11+
612
It is common in Qt programming to override virtual C++ methods to customize
713
behavior, like listening for mouse events, implement drawing routines, etc.
814

@@ -76,3 +82,9 @@ This might be desirable if you plan to install a custom exception hook.
7682
actually trigger an ``abort()``, crashing the Python interpreter. For this
7783
reason, disabling exception capture in ``PyQt5.5+`` and ``PyQt6`` is not
7884
recommended unless you install your own exception hook.
85+
86+
.. note::
87+
88+
As explained in the note at the top of the page, ``PySide6 6.5.2+`` has its own
89+
exception capture mechanism, so this option has no effect when using this
90+
library.

tests/test_exceptions.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
import pytest
44

55
from pytestqt.exceptions import capture_exceptions, format_captured_exceptions
6+
from pytestqt.qt_compat import qt_api
7+
8+
# PySide6 is automatically captures exceptions during the event loop,
9+
# and re-raises them when control gets back to Python, so the related
10+
# functionality does not work, nor is needed for the end user.
11+
exception_capture_pyside6 = pytest.mark.skipif(
12+
qt_api.pytest_qt_api == "pyside6",
13+
reason="pytest-qt capture not working/needed on PySide6",
14+
)
615

716

817
@pytest.mark.parametrize("raise_error", [False, True])
@@ -42,10 +51,24 @@ def test_exceptions(qtbot):
4251
)
4352
result = testdir.runpytest()
4453
if raise_error:
45-
expected_lines = ["*Exceptions caught in Qt event loop:*"]
46-
if sys.version_info.major == 3:
47-
expected_lines.append("RuntimeError: original error")
48-
expected_lines.extend(["*ValueError: mistakes were made*", "*1 failed*"])
54+
if qt_api.pytest_qt_api == "pyside6":
55+
# PySide6 automatically captures exceptions during the event loop,
56+
# and re-raises them when control gets back to Python.
57+
# This results in the exception not being captured by
58+
# us, and a more natural traceback which includes the app.sendEvent line.
59+
expected_lines = [
60+
"*RuntimeError: original error",
61+
"*app.sendEvent*",
62+
"*ValueError: mistakes were made*",
63+
"*1 failed*",
64+
]
65+
else:
66+
expected_lines = [
67+
"*Exceptions caught in Qt event loop:*",
68+
"RuntimeError: original error",
69+
"*ValueError: mistakes were made*",
70+
"*1 failed*",
71+
]
4972
result.stdout.fnmatch_lines(expected_lines)
5073
assert "pytest.fail" not in "\n".join(result.outlines)
5174
else:
@@ -84,6 +107,7 @@ def test_format_captured_exceptions_chained():
84107

85108

86109
@pytest.mark.parametrize("no_capture_by_marker", [True, False])
110+
@exception_capture_pyside6
87111
def test_no_capture(testdir, no_capture_by_marker):
88112
"""
89113
Make sure options that disable exception capture are working (either marker
@@ -99,15 +123,15 @@ def test_no_capture(testdir, no_capture_by_marker):
99123
"""
100124
[pytest]
101125
qt_no_exception_capture = 1
102-
"""
126+
"""
103127
)
104128
testdir.makepyfile(
105-
"""
129+
f"""
106130
import pytest
107131
import sys
108132
from pytestqt.qt_compat import qt_api
109133
110-
# PyQt 5.5+ will crash if there's no custom exception handler installed
134+
# PyQt 5.5+ will crash if there's no custom exception handler installed.
111135
sys.excepthook = lambda *args: None
112136
113137
class MyWidget(qt_api.QtWidgets.QWidget):
@@ -120,9 +144,7 @@ def test_widget(qtbot):
120144
w = MyWidget()
121145
qtbot.addWidget(w)
122146
qtbot.mouseClick(w, qt_api.QtCore.Qt.MouseButton.LeftButton)
123-
""".format(
124-
marker_code=marker_code
125-
)
147+
"""
126148
)
127149
res = testdir.runpytest()
128150
res.stdout.fnmatch_lines(["*1 passed*"])
@@ -265,6 +287,7 @@ def test_capture(widget):
265287

266288

267289
@pytest.mark.qt_no_exception_capture
290+
@exception_capture_pyside6
268291
def test_capture_exceptions_context_manager(qapp):
269292
"""Test capture_exceptions() context manager.
270293
@@ -319,6 +342,7 @@ def raise_on_event():
319342
result.stdout.fnmatch_lines(["*1 passed*"])
320343

321344

345+
@exception_capture_pyside6
322346
def test_exceptions_to_stderr(qapp, capsys):
323347
"""
324348
Exceptions should still be reported to stderr.
@@ -341,10 +365,7 @@ def event(self, ev):
341365
assert 'raise RuntimeError("event processed")' in err
342366

343367

344-
@pytest.mark.xfail(
345-
condition=sys.version_info[:2] == (3, 4),
346-
reason="failing in Python 3.4, which is about to be dropped soon anyway",
347-
)
368+
@exception_capture_pyside6
348369
def test_exceptions_dont_leak(testdir):
349370
"""
350371
Ensure exceptions are cleared when an exception occurs and don't leak (#187).

0 commit comments

Comments
 (0)