From a26733cb6e378e3f4cad1377e15b580a444e29a9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 10:08:40 +0100 Subject: [PATCH 1/6] Revert "Enable qInfo tests for PySide6 (#593)" This reverts commit ce1a689ed13b63a4aef3d25825957430d8776644. This change is incorrect as it breaks things with PySide < 6.8.2, but we support running against older versions. The next commit will fix this properly without breaking compatibility. See #232 --- src/pytestqt/qt_compat.py | 9 ++++++++- tests/test_basics.py | 1 - tests/test_logging.py | 30 +++++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index 3b9aef7..d3aafde 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -111,7 +111,14 @@ def _import_module(module_name): self._check_qt_api_version() - self.qInfo = QtCore.qInfo + # qInfo is not exposed in PySide6 (#232) + if hasattr(QtCore, "QMessageLogger"): + self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) + elif hasattr(QtCore, "qInfo"): + self.qInfo = QtCore.qInfo + else: + self.qInfo = None + self.qDebug = QtCore.qDebug self.qWarning = QtCore.qWarning self.qCritical = QtCore.qCritical diff --git a/tests/test_basics.py b/tests/test_basics.py index 6b577f5..85e24f1 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -613,7 +613,6 @@ class Mock: qtcore = Mock() for method_name in ( "qInstallMessageHandler", - "qInfo", "qDebug", "qWarning", "qCritical", diff --git a/tests/test_logging.py b/tests/test_logging.py index d5ad134..80b694c 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -26,7 +26,8 @@ def print_msg(msg_type, context, message): qt_api.QtCore.qInstallMessageHandler(print_msg) def test_types(): - qt_api.qInfo('this is an INFO message') + # qInfo is not exposed by the bindings yet (#225) + # qt_api.qInfo('this is an INFO message') qt_api.qDebug('this is a DEBUG message') qt_api.qWarning('this is a WARNING message') qt_api.qCritical('this is a CRITICAL message') @@ -44,7 +45,8 @@ def test_types(): res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", - "*QtInfoMsg: this is an INFO message*", + # qInfo is not exposed by the bindings yet (#232) + # '*QtInfoMsg: this is an INFO message*', "*QtDebugMsg: this is a DEBUG message*", "*QtWarningMsg: this is a WARNING message*", "*QtCriticalMsg: this is a CRITICAL message*", @@ -54,7 +56,9 @@ def test_types(): res.stdout.fnmatch_lines( [ "*-- Captured stderr call --*", - "this is an INFO message*", + # qInfo is not exposed by the bindings yet (#232) + # '*QtInfoMsg: this is an INFO message*', + # 'this is an INFO message*', "this is a DEBUG message*", "this is a WARNING message*", "this is a CRITICAL message*", @@ -62,17 +66,33 @@ def test_types(): ) +def test_qinfo(qtlog): + """Test INFO messages when we have means to do so. Should be temporary until bindings + catch up and expose qInfo (or at least QMessageLogger), then we should update + the other logging tests properly. #232 + """ + + if qt_api.is_pyside: + assert ( + qt_api.qInfo is None + ), "pyside6 does not expose qInfo. If it does, update this test." + return + + qt_api.qInfo("this is an INFO message") + records = [(m.type, m.message.strip()) for m in qtlog.records] + assert records == [(qt_api.QtCore.QtMsgType.QtInfoMsg, "this is an INFO message")] + + def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ - qt_api.qInfo("this is an INFO message") + # qInfo is not exposed by the bindings yet (#232) qt_api.qDebug("this is a DEBUG message") qt_api.qWarning("this is a WARNING message") qt_api.qCritical("this is a CRITICAL message") records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [ - (qt_api.QtCore.QtMsgType.QtInfoMsg, "this is an INFO message"), (qt_api.QtCore.QtMsgType.QtDebugMsg, "this is a DEBUG message"), (qt_api.QtCore.QtMsgType.QtWarningMsg, "this is a WARNING message"), (qt_api.QtCore.QtMsgType.QtCriticalMsg, "this is a CRITICAL message"), From 40d237a8b16b1a938abed871e8ddd99865b902e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 10:35:24 +0100 Subject: [PATCH 2/6] tests: Make qInfo tests work with PySide6 6.8.2+ qInfo() has been added: https://codereview.qt-project.org/c/pyside/pyside-setup/+/605100 Follow-up / alternative to #593 without breaking compatibility with older PySide6 versions. See #232. --- src/pytestqt/qt_compat.py | 2 +- tests/test_logging.py | 53 ++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index d3aafde..f98a5f1 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -111,7 +111,7 @@ def _import_module(module_name): self._check_qt_api_version() - # qInfo is not exposed in PySide6 (#232) + # qInfo is not exposed in PySide6 < 6.8.2 (#232) if hasattr(QtCore, "QMessageLogger"): self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) elif hasattr(QtCore, "qInfo"): diff --git a/tests/test_logging.py b/tests/test_logging.py index 80b694c..f1fafbd 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -5,6 +5,10 @@ from pytestqt.qt_compat import qt_api +# qInfo is not exposed by PySide6 < 6.8.2 (#225) +HAS_QINFO = qt_api.qInfo is not None + + @pytest.mark.parametrize("test_succeeds", [True, False]) @pytest.mark.parametrize("qt_log", [True, False]) def test_basic_logging(testdir, test_succeeds, qt_log): @@ -14,7 +18,7 @@ def test_basic_logging(testdir, test_succeeds, qt_log): :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile( - """ + f""" import sys from pytestqt.qt_compat import qt_api @@ -26,15 +30,13 @@ def print_msg(msg_type, context, message): qt_api.QtCore.qInstallMessageHandler(print_msg) def test_types(): - # qInfo is not exposed by the bindings yet (#225) - # qt_api.qInfo('this is an INFO message') + if {HAS_QINFO}: + qt_api.qInfo('this is an INFO message') qt_api.qDebug('this is a DEBUG message') qt_api.qWarning('this is a WARNING message') qt_api.qCritical('this is a CRITICAL message') - assert {} - """.format( - test_succeeds - ) + assert {test_succeeds} + """ ) res = testdir.runpytest(*(["--no-qt-log"] if not qt_log else [])) if test_succeeds: @@ -45,8 +47,7 @@ def test_types(): res.stdout.fnmatch_lines( [ "*-- Captured Qt messages --*", - # qInfo is not exposed by the bindings yet (#232) - # '*QtInfoMsg: this is an INFO message*', + *(["*QtInfoMsg: this is an INFO message*"] if HAS_QINFO else []), "*QtDebugMsg: this is a DEBUG message*", "*QtWarningMsg: this is a WARNING message*", "*QtCriticalMsg: this is a CRITICAL message*", @@ -56,9 +57,7 @@ def test_types(): res.stdout.fnmatch_lines( [ "*-- Captured stderr call --*", - # qInfo is not exposed by the bindings yet (#232) - # '*QtInfoMsg: this is an INFO message*', - # 'this is an INFO message*', + *(["this is an INFO message*"] if HAS_QINFO else []), "this is a DEBUG message*", "this is a WARNING message*", "this is a CRITICAL message*", @@ -66,37 +65,27 @@ def test_types(): ) -def test_qinfo(qtlog): - """Test INFO messages when we have means to do so. Should be temporary until bindings - catch up and expose qInfo (or at least QMessageLogger), then we should update - the other logging tests properly. #232 - """ - - if qt_api.is_pyside: - assert ( - qt_api.qInfo is None - ), "pyside6 does not expose qInfo. If it does, update this test." - return - - qt_api.qInfo("this is an INFO message") - records = [(m.type, m.message.strip()) for m in qtlog.records] - assert records == [(qt_api.QtCore.QtMsgType.QtInfoMsg, "this is an INFO message")] - - def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ - # qInfo is not exposed by the bindings yet (#232) + expected = [] + if HAS_QINFO: + qt_api.qInfo("this is an INFO message") + expected.append((qt_api.QtCore.QtMsgType.QtInfoMsg, "this is an INFO message")) + qt_api.qDebug("this is a DEBUG message") qt_api.qWarning("this is a WARNING message") qt_api.qCritical("this is a CRITICAL message") - records = [(m.type, m.message.strip()) for m in qtlog.records] - assert records == [ + + expected += [ (qt_api.QtCore.QtMsgType.QtDebugMsg, "this is a DEBUG message"), (qt_api.QtCore.QtMsgType.QtWarningMsg, "this is a WARNING message"), (qt_api.QtCore.QtMsgType.QtCriticalMsg, "this is a CRITICAL message"), ] + + records = [(m.type, m.message.strip()) for m in qtlog.records] + assert records == expected # `records` attribute is read-only with pytest.raises(AttributeError): qtlog.records = [] From 917872f4352cd39733f55c71c90a81e8f2e99df2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 10:40:53 +0100 Subject: [PATCH 3/6] tests: Fix exception capture tests with older PySide6 Follow-up to #525 to conditionally do the right thing based on the PySide6 version, so we can continue to run tests with older versions. --- tests/test_exceptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 8627f9b..9347b9d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -5,11 +5,18 @@ from pytestqt.exceptions import capture_exceptions, format_captured_exceptions from pytestqt.qt_compat import qt_api + +def has_pyside6_exception_capture(): + return qt_api.pytest_qt_api == "pyside6" and tuple( + int(part) for part in qt_api.get_versions().qt_api_version.split(".") + ) >= (6, 5, 2) + + # 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", + has_pyside6_exception_capture(), reason="pytest-qt capture not working/needed on PySide6", ) @@ -51,7 +58,7 @@ def test_exceptions(qtbot): ) result = testdir.runpytest() if raise_error: - if qt_api.pytest_qt_api == "pyside6": + if has_pyside6_exception_capture(): # 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 From e648ad621f9041c53d8fb97aea5ea7e6efcc0124 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 10:42:28 +0100 Subject: [PATCH 4/6] ci: Run tests with oldest supported PySide/PyQt versions We support older versions of PySide6/PyQt6, so we should also test with the oldest versions we support to prevent regressions. --- .github/workflows/test.yml | 7 +++++++ tox.ini | 5 ++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 140b0af..3afb410 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,13 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] qt-lib: [pyqt5, pyqt6, pyside6] os: [ubuntu-latest, windows-latest, macos-latest] + include: + - python-version: "3.9" + qt-lib: pyqt61 + os: ubuntu-latest + - python-version: "3.9" + qt-lib: pyside60 + os: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/tox.ini b/tox.ini index 09abcb7..6f97c96 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,15 @@ [tox] -envlist = py{39,310,311,312,313}-{pyqt5,pyside6,pyqt6} +envlist = py{39,310,311,312,313}-{pyqt5,pyside6,pyqt6},py39-pyside60,py39-pyqt61 [testenv] deps= pytest pyside6: pyside6 + pyside60: pyside6<6.1 pyqt5: pyqt5 pyqt6: pyqt6 + pyqt61: pyqt6<6.2 + pyqt61: pyqt6-sip<13.6 commands= pytest --color=yes {posargs} setenv= From fa95d0d718254d107cbf0131d0215859b954b524 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 11:06:53 +0100 Subject: [PATCH 5/6] tests: Fix test_header patching leaking into other tests Since we run pytester in-process, the patching of qt_api.get_versions will leak into the rest of the testsuite (even if done in a temporary conftest.py, the qt_api module is the same!). This causes Qt API version checks in the testsuite to be broken. --- tests/test_basics.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/test_basics.py b/tests/test_basics.py index 85e24f1..49cde4b 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -309,20 +309,13 @@ def test_events(events_queue, fix, i): res.stdout.fnmatch_lines(["*3 passed in*"]) -def test_header(testdir): - testdir.makeconftest( - """ - from pytestqt import qt_compat - from pytestqt.qt_compat import qt_api - - def mock_get_versions(): - return qt_compat.VersionTuple('PyQtAPI', '1.0', '2.5', '3.5') - - assert hasattr(qt_api, 'get_versions') - qt_api.get_versions = mock_get_versions - """ +def test_header(testdir, monkeypatch): + monkeypatch.setattr( + qt_api, + "get_versions", + lambda: qt_compat.VersionTuple("PyQtAPI", "1.0", "2.5", "3.5"), ) - res = testdir.runpytest() + res = testdir.runpytest_inprocess() res.stdout.fnmatch_lines( ["*test session starts*", "PyQtAPI 1.0 -- Qt runtime 2.5 -- Qt compiled 3.5"] ) From 2a87beaeeba9a5da434b7ad91f8ca3e2e12e629b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 25 Mar 2025 11:19:15 +0100 Subject: [PATCH 6/6] qt_compat: Prefer real qInfo to QMessageLogger shim --- src/pytestqt/qt_compat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index f98a5f1..e66f369 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -24,7 +24,7 @@ def _import(name): - """Think call so we can mock it during testing""" + """Thin call so we can mock it during testing""" return __import__(name) @@ -112,10 +112,10 @@ def _import_module(module_name): self._check_qt_api_version() # qInfo is not exposed in PySide6 < 6.8.2 (#232) - if hasattr(QtCore, "QMessageLogger"): - self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) - elif hasattr(QtCore, "qInfo"): + if hasattr(QtCore, "qInfo"): self.qInfo = QtCore.qInfo + elif hasattr(QtCore, "QMessageLogger"): + self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg) else: self.qInfo = None