Skip to content

Commit 62c36c1

Browse files
authored
Merge pull request #525 from nicoddemus/pyside6-capture
Add note about PySide6 exception capture and fix tests
2 parents 6b4639f + 878296e commit 62c36c1

File tree

4 files changed

+50
-15
lines changed

4 files changed

+50
-15
lines changed

.readthedocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ sphinx:
1111
python:
1212
install:
1313
- path: .
14+
- requirements: docs/requirements.txt

docs/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sphinx
2+
sphinx-rtd-theme

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 & 15 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:
@@ -65,7 +88,6 @@ def test_format_captured_exceptions():
6588
assert "ValueError: errors were made" in lines
6689

6790

68-
@pytest.mark.skipif(sys.version_info.major == 2, reason="Python 3 only")
6991
def test_format_captured_exceptions_chained():
7092
try:
7193
try:
@@ -84,6 +106,7 @@ def test_format_captured_exceptions_chained():
84106

85107

86108
@pytest.mark.parametrize("no_capture_by_marker", [True, False])
109+
@exception_capture_pyside6
87110
def test_no_capture(testdir, no_capture_by_marker):
88111
"""
89112
Make sure options that disable exception capture are working (either marker
@@ -99,15 +122,15 @@ def test_no_capture(testdir, no_capture_by_marker):
99122
"""
100123
[pytest]
101124
qt_no_exception_capture = 1
102-
"""
125+
"""
103126
)
104127
testdir.makepyfile(
105-
"""
128+
f"""
106129
import pytest
107130
import sys
108131
from pytestqt.qt_compat import qt_api
109132
110-
# PyQt 5.5+ will crash if there's no custom exception handler installed
133+
# PyQt 5.5+ will crash if there's no custom exception handler installed.
111134
sys.excepthook = lambda *args: None
112135
113136
class MyWidget(qt_api.QtWidgets.QWidget):
@@ -120,9 +143,7 @@ def test_widget(qtbot):
120143
w = MyWidget()
121144
qtbot.addWidget(w)
122145
qtbot.mouseClick(w, qt_api.QtCore.Qt.MouseButton.LeftButton)
123-
""".format(
124-
marker_code=marker_code
125-
)
146+
"""
126147
)
127148
res = testdir.runpytest()
128149
res.stdout.fnmatch_lines(["*1 passed*"])
@@ -265,6 +286,7 @@ def test_capture(widget):
265286

266287

267288
@pytest.mark.qt_no_exception_capture
289+
@exception_capture_pyside6
268290
def test_capture_exceptions_context_manager(qapp):
269291
"""Test capture_exceptions() context manager.
270292
@@ -319,6 +341,7 @@ def raise_on_event():
319341
result.stdout.fnmatch_lines(["*1 passed*"])
320342

321343

344+
@exception_capture_pyside6
322345
def test_exceptions_to_stderr(qapp, capsys):
323346
"""
324347
Exceptions should still be reported to stderr.
@@ -341,10 +364,7 @@ def event(self, ev):
341364
assert 'raise RuntimeError("event processed")' in err
342365

343366

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-
)
367+
@exception_capture_pyside6
348368
def test_exceptions_dont_leak(testdir):
349369
"""
350370
Ensure exceptions are cleared when an exception occurs and don't leak (#187).

0 commit comments

Comments
 (0)