Skip to content

Commit

Permalink
Add note about PySide6 exception capture and fix tests
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nicoddemus committed Oct 10, 2023
1 parent 6b4639f commit c64a0d2
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 14 deletions.
12 changes: 12 additions & 0 deletions docs/virtual_methods.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Exceptions in virtual methods

.. versionadded:: 1.1

.. note::

``PySide6`` automatically captures exceptions that happen during the Qt event loop and
re-raises them when control is moved back to Python, so the functionality described here
does not work with ``PySide6`` (nor is necessary).

It is common in Qt programming to override virtual C++ methods to customize
behavior, like listening for mouse events, implement drawing routines, etc.

Expand Down Expand Up @@ -76,3 +82,9 @@ This might be desirable if you plan to install a custom exception hook.
actually trigger an ``abort()``, crashing the Python interpreter. For this
reason, disabling exception capture in ``PyQt5.5+`` and ``PyQt6`` is not
recommended unless you install your own exception hook.

.. note::

As explained in the note at the top of the page, ``PySide6`` has its own
exception capture mechanism, so this option has no effect when using this
library.
49 changes: 35 additions & 14 deletions tests/test_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
import pytest

from pytestqt.exceptions import capture_exceptions, format_captured_exceptions
from pytestqt.qt_compat import qt_api

# PySide6 is automatically captures exceptions during the event loop,
# and re-raises them when control gets back to Python, so the related
# functionality does not work, nor is needed for the end user.
exception_capture_pyside6 = pytest.mark.skipif(
qt_api.pytest_qt_api == "pyside6",
reason="pytest-qt capture not working/needed on PySide6",
)


@pytest.mark.parametrize("raise_error", [False, True])
Expand Down Expand Up @@ -42,10 +51,24 @@ def test_exceptions(qtbot):
)
result = testdir.runpytest()
if raise_error:
expected_lines = ["*Exceptions caught in Qt event loop:*"]
if sys.version_info.major == 3:
expected_lines.append("RuntimeError: original error")
expected_lines.extend(["*ValueError: mistakes were made*", "*1 failed*"])
if qt_api.pytest_qt_api == "pyside6":
# PySide6 automatically captures exceptions during the event loop,
# and re-raises them when control gets back to Python.
# This results in the exception not being captured by
# us, and a more natural traceback which includes the app.sendEvent line.
expected_lines = [
"*RuntimeError: original error",
"*app.sendEvent*",
"*ValueError: mistakes were made*",
"*1 failed*",
]
else:
expected_lines = [
"*Exceptions caught in Qt event loop:*",
"RuntimeError: original error",
"*ValueError: mistakes were made*",
"*1 failed*",
]
result.stdout.fnmatch_lines(expected_lines)
assert "pytest.fail" not in "\n".join(result.outlines)
else:
Expand Down Expand Up @@ -84,6 +107,7 @@ def test_format_captured_exceptions_chained():


@pytest.mark.parametrize("no_capture_by_marker", [True, False])
@exception_capture_pyside6
def test_no_capture(testdir, no_capture_by_marker):
"""
Make sure options that disable exception capture are working (either marker
Expand All @@ -99,15 +123,15 @@ def test_no_capture(testdir, no_capture_by_marker):
"""
[pytest]
qt_no_exception_capture = 1
"""
"""
)
testdir.makepyfile(
"""
f"""
import pytest
import sys
from pytestqt.qt_compat import qt_api
# PyQt 5.5+ will crash if there's no custom exception handler installed
# PyQt 5.5+ will crash if there's no custom exception handler installed.
sys.excepthook = lambda *args: None
class MyWidget(qt_api.QtWidgets.QWidget):
Expand All @@ -120,9 +144,7 @@ def test_widget(qtbot):
w = MyWidget()
qtbot.addWidget(w)
qtbot.mouseClick(w, qt_api.QtCore.Qt.MouseButton.LeftButton)
""".format(
marker_code=marker_code
)
"""
)
res = testdir.runpytest()
res.stdout.fnmatch_lines(["*1 passed*"])
Expand Down Expand Up @@ -265,6 +287,7 @@ def test_capture(widget):


@pytest.mark.qt_no_exception_capture
@exception_capture_pyside6
def test_capture_exceptions_context_manager(qapp):
"""Test capture_exceptions() context manager.
Expand Down Expand Up @@ -319,6 +342,7 @@ def raise_on_event():
result.stdout.fnmatch_lines(["*1 passed*"])


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


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

0 comments on commit c64a0d2

Please sign in to comment.