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

Add note about PySide6 exception capture and fix tests #525

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ sphinx:
python:
install:
- path: .
- requirements: docs/requirements.txt
2 changes: 2 additions & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
sphinx
sphinx-rtd-theme
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 6.5.2+`` 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 6.5.2+`` has its own
exception capture mechanism, so this option has no effect when using this
library.
50 changes: 35 additions & 15 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 All @@ -65,7 +88,6 @@ def test_format_captured_exceptions():
assert "ValueError: errors were made" in lines


@pytest.mark.skipif(sys.version_info.major == 2, reason="Python 3 only")
def test_format_captured_exceptions_chained():
try:
try:
Expand All @@ -84,6 +106,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 +122,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 +143,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 +286,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 +341,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 +364,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