From 1df7b94f2acd9560294cef40779e0255acb82c4c Mon Sep 17 00:00:00 2001 From: Nir <88795475+nrbnlulu@users.noreply.github.com> Date: Wed, 1 Feb 2023 19:11:51 +0200 Subject: [PATCH 1/3] dep: added qtpy --- .gitignore | 2 ++ .pre-commit-config.yaml | 2 +- setup.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 2e797d0e..529a3a98 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ src/pytest_qt.egg-info # auto-generated by setuptools_scm /src/pytestqt/_version.py +# PyCharm +.idea diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d12cf56..5768dbdf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,7 +28,7 @@ repos: rev: 1.13.0 hooks: - id: blacken-docs - additional_dependencies: [black==20.8b1] + additional_dependencies: [black] language_version: python3 - repo: https://github.com/PyCQA/flake8 rev: 6.0.0 diff --git a/setup.py b/setup.py index a33eabfc..c605831e 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ packages=find_packages(where="src"), package_dir={"": "src"}, entry_points={"pytest11": ["pytest-qt = pytestqt.plugin"]}, - install_requires=["pytest>=3.0.0"], + install_requires=["pytest>=3.0.0", "qtpy"], extras_require={ "doc": ["sphinx", "sphinx_rtd_theme"], "dev": ["pre-commit", "tox"], From a3423ea0979dca77ea0a83485befb301e08c7505 Mon Sep 17 00:00:00 2001 From: Nir <88795475+nrbnlulu@users.noreply.github.com> Date: Wed, 1 Feb 2023 19:49:59 +0200 Subject: [PATCH 2/3] pc --- src/pytestqt/plugin.py | 17 +-- src/pytestqt/qt_compat.py | 209 +++--------------------------------- src/pytestqt/qtbot.py | 2 +- src/pytestqt/wait_signal.py | 4 +- tox.ini | 1 + 5 files changed, 20 insertions(+), 213 deletions(-) diff --git a/src/pytestqt/plugin.py b/src/pytestqt/plugin.py index fab2c723..3af25286 100644 --- a/src/pytestqt/plugin.py +++ b/src/pytestqt/plugin.py @@ -1,3 +1,4 @@ +import os import warnings import pytest @@ -226,18 +227,4 @@ def pytest_configure(config): if config.getoption("qt_log") and config.getoption("capture") != "no": config.pluginmanager.register(QtLoggingPlugin(config), "_qt_logging") - - qt_api.set_qt_api(config.getini("qt_api")) - - -def pytest_report_header(): - from pytestqt.qt_compat import qt_api - - v = qt_api.get_versions() - fields = [ - f"{v.qt_api} {v.qt_api_version}", - "Qt runtime %s" % v.runtime, - "Qt compiled %s" % v.compiled, - ] - version_line = " -- ".join(fields) - return [version_line] + os.environ.setdefault("QT_API", config.getini("qt_api")) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index e0c3a350..70fd1863 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -1,200 +1,19 @@ -""" -Provide a common way to import Qt classes used by pytest-qt in a unique manner, -abstracting API differences between PyQt5/6 and PySide2/6. +import qtpy -.. note:: This module is not part of pytest-qt public API, hence its interface -may change between releases and users should not rely on it. -Based on from https://github.com/epage/PythonUtils. -""" +from qtpy import QtTest, QtCore, QtWidgets, QtQuick, QtQml, QtGui -from collections import namedtuple, OrderedDict -import os -import sys +class qt_api: + QtCore = QtCore + QtTest = QtTest + QtGui = QtGui + QtWidgets = QtWidgets + QtQuick = QtQuick + QtQml = QtQml + pytest_qt_api = qtpy.API + qDebug = QtCore.qDebug -import pytest - - -VersionTuple = namedtuple("VersionTuple", "qt_api, qt_api_version, runtime, compiled") - -QT_APIS = OrderedDict() -QT_APIS["pyside6"] = "PySide6" -QT_APIS["pyside2"] = "PySide2" -QT_APIS["pyqt6"] = "PyQt6" -QT_APIS["pyqt5"] = "PyQt5" - - -def _import(name): - """Think call so we can mock it during testing""" - return __import__(name) - - -def _is_library_loaded(name): - return name in sys.modules - - -class _QtApi: - """ - Interface to the underlying Qt API currently configured for pytest-qt. - - This object lazily loads all class references and other objects when the ``set_qt_api`` method - gets called, providing a uniform way to access the Qt classes. - """ - - def __init__(self): - self._import_errors = {} - - def _get_qt_api_from_env(self): - api = os.environ.get("PYTEST_QT_API") - supported_apis = QT_APIS.keys() - - if api is not None: - api = api.lower() - if api not in supported_apis: # pragma: no cover - msg = f"Invalid value for $PYTEST_QT_API: {api}, expected one of {supported_apis}" - raise pytest.UsageError(msg) - return api - - def _get_already_loaded_backend(self): - for api, backend in QT_APIS.items(): - if _is_library_loaded(backend): - return api - return None - - def _guess_qt_api(self): # pragma: no cover - def _can_import(name): - try: - _import(name) - return True - except ModuleNotFoundError as e: - self._import_errors[name] = str(e) - return False - - # Note, not importing only the root namespace because when uninstalling from conda, - # the namespace can still be there. - for api, backend in QT_APIS.items(): - if _can_import(f"{backend}.QtCore"): - return api - return None - - def set_qt_api(self, api): - self.pytest_qt_api = ( - self._get_qt_api_from_env() - or api - or self._get_already_loaded_backend() - or self._guess_qt_api() - ) - - self.is_pyside = self.pytest_qt_api in ["pyside2", "pyside6"] - self.is_pyqt = self.pytest_qt_api in ["pyqt5", "pyqt6"] - - if not self.pytest_qt_api: # pragma: no cover - errors = "\n".join( - f" {module}: {reason}" - for module, reason in sorted(self._import_errors.items()) - ) - msg = ( - "pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n" - + errors - ) - raise pytest.UsageError(msg) - - _root_module = QT_APIS[self.pytest_qt_api] - - def _import_module(module_name): - m = __import__(_root_module, globals(), locals(), [module_name], 0) - return getattr(m, module_name) - - self.QtCore = QtCore = _import_module("QtCore") - self.QtGui = _import_module("QtGui") - self.QtTest = _import_module("QtTest") - self.QtWidgets = _import_module("QtWidgets") - - self._check_qt_api_version() - - # qInfo is not exposed in PySide2/6 (#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 - self.qFatal = QtCore.qFatal - - if self.is_pyside: - self.Signal = QtCore.Signal - self.Slot = QtCore.Slot - self.Property = QtCore.Property - elif self.is_pyqt: - self.Signal = QtCore.pyqtSignal - self.Slot = QtCore.pyqtSlot - self.Property = QtCore.pyqtProperty - else: - assert False, "Expected either is_pyqt or is_pyside" - - def _check_qt_api_version(self): - if not self.is_pyqt: - # We support all PySide versions - return - - if self.QtCore.PYQT_VERSION == 0x060000: # 6.0.0 - raise pytest.UsageError( - "PyQt 6.0 is not supported by pytest-qt, use 6.1+ instead." - ) - elif self.QtCore.PYQT_VERSION < 0x050B00: # 5.11.0 - raise pytest.UsageError( - "PyQt < 5.11 is not supported by pytest-qt, use 5.11+ instead." - ) - - def exec(self, obj, *args, **kwargs): - # exec was a keyword in Python 2, so PySide2 (and also PySide6 6.0) - # name the corresponding method "exec_" instead. - # - # The old _exec() alias is removed in PyQt6 and also deprecated as of - # PySide 6.1: - # https://codereview.qt-project.org/c/pyside/pyside-setup/+/342095 - if hasattr(obj, "exec"): - return obj.exec(*args, **kwargs) - return obj.exec_(*args, **kwargs) - - def get_versions(self): - if self.pytest_qt_api == "pyside6": - import PySide6 - - version = PySide6.__version__ - - return VersionTuple( - "PySide6", version, self.QtCore.qVersion(), self.QtCore.__version__ - ) - elif self.pytest_qt_api == "pyside2": - import PySide2 - - version = PySide2.__version__ - - return VersionTuple( - "PySide2", version, self.QtCore.qVersion(), self.QtCore.__version__ - ) - elif self.pytest_qt_api == "pyqt6": - return VersionTuple( - "PyQt6", - self.QtCore.PYQT_VERSION_STR, - self.QtCore.qVersion(), - self.QtCore.QT_VERSION_STR, - ) - elif self.pytest_qt_api == "pyqt5": - return VersionTuple( - "PyQt5", - self.QtCore.PYQT_VERSION_STR, - self.QtCore.qVersion(), - self.QtCore.QT_VERSION_STR, - ) - - assert False, f"Internal error, unknown pytest_qt_api: {self.pytest_qt_api}" - - -qt_api = _QtApi() + @staticmethod + def is_pyside() -> bool: + return qtpy.API in (qtpy.PYSIDE2, qtpy.PYSIDE6) diff --git a/src/pytestqt/qtbot.py b/src/pytestqt/qtbot.py index 3c65f72d..c28f33dc 100644 --- a/src/pytestqt/qtbot.py +++ b/src/pytestqt/qtbot.py @@ -283,7 +283,7 @@ def stop(self): if widget is not None: widget_and_visibility.append((widget, widget.isVisible())) - qt_api.exec(qt_api.QtWidgets.QApplication.instance()) + qt_api.QtWidgets.QApplication.instance().exec() for widget, visible in widget_and_visibility: widget.setVisible(visible) diff --git a/src/pytestqt/wait_signal.py b/src/pytestqt/wait_signal.py index 12cfba40..cab3a4b7 100644 --- a/src/pytestqt/wait_signal.py +++ b/src/pytestqt/wait_signal.py @@ -48,7 +48,7 @@ def wait(self): self._timer.start() if self.timeout != 0: - qt_api.exec(self._loop) + self._loop.exec() if not self.signal_triggered and self.raising: raise TimeoutError(self._timeout_message) @@ -668,7 +668,7 @@ def wait(self): if self._timer is not None: self._timer.timeout.connect(self._quit_loop_by_timeout) self._timer.start() - qt_api.exec(self._loop) + self._loop.exec() if not self.called and self.raising: raise TimeoutError("Callback wasn't called after %sms." % self.timeout) diff --git a/tox.ini b/tox.ini index fc86029d..796852f0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,6 +4,7 @@ envlist = py{37,38,39,310}-{pyqt5,pyside2,pyside6,pyqt6}, linting [testenv] deps= pytest + qtpy pyside6: pyside6 pyside2: pyside2 pyqt5: pyqt5 From a224a138309513af9cd7119e70db1cbda80a9129 Mon Sep 17 00:00:00 2001 From: Nir <88795475+nrbnlulu@users.noreply.github.com> Date: Wed, 1 Feb 2023 20:10:47 +0200 Subject: [PATCH 3/3] most tests pass. --- src/pytestqt/qt_compat.py | 11 ++++++++--- tests/test_basics.py | 18 +++++++++--------- tests/test_modeltest.py | 2 +- tests/test_wait_signal.py | 4 ++-- 4 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/pytestqt/qt_compat.py b/src/pytestqt/qt_compat.py index 70fd1863..ab2c50c1 100644 --- a/src/pytestqt/qt_compat.py +++ b/src/pytestqt/qt_compat.py @@ -5,6 +5,7 @@ class qt_api: + QT_API = qtpy.API QtCore = QtCore QtTest = QtTest QtGui = QtGui @@ -12,8 +13,12 @@ class qt_api: QtQuick = QtQuick QtQml = QtQml pytest_qt_api = qtpy.API + Signal = QtCore.Signal + qDebug = QtCore.qDebug + qCritical = QtCore.qCritical + qInfo = QtCore.qInfo + qWarning = QtCore.qWarning + is_pyside = qtpy.API in (qtpy.PYSIDE2, qtpy.PYSIDE6) - @staticmethod - def is_pyside() -> bool: - return qtpy.API in (qtpy.PYSIDE2, qtpy.PYSIDE6) + is_pyqt = not is_pyside diff --git a/tests/test_basics.py b/tests/test_basics.py index e14fd65e..9828786e 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -446,7 +446,7 @@ def test_qt_api_ini_config(testdir, monkeypatch, option_api): """ from pytestqt.qt_compat import qt_api - monkeypatch.delenv("PYTEST_QT_API", raising=False) + monkeypatch.delenv("QT_API", raising=False) testdir.makeini( """ @@ -467,7 +467,7 @@ def test_foo(qtbot): ) result = testdir.runpytest_subprocess() - if qt_api.pytest_qt_api == option_api: + if qt_api.QT_API == option_api: result.stdout.fnmatch_lines(["* 1 passed in *"]) else: try: @@ -492,7 +492,7 @@ def test_qt_api_ini_config_with_envvar(testdir, monkeypatch, envvar): ) ) - monkeypatch.setenv("PYTEST_QT_API", envvar) + monkeypatch.setenv("QT_API", envvar) testdir.makepyfile( """ @@ -504,7 +504,7 @@ def test_foo(qtbot): ) result = testdir.runpytest_subprocess() - if qt_api.pytest_qt_api == envvar: + if qt_api.QT_API == envvar: result.stdout.fnmatch_lines(["* 1 passed in *"]) else: try: @@ -529,10 +529,10 @@ def test_foo(qtbot): pass """ ) - monkeypatch.setenv("PYTEST_QT_API", "piecute") + monkeypatch.setenv("QT_API", "piecute") result = testdir.runpytest_subprocess() result.stderr.fnmatch_lines( - ["* Invalid value for $PYTEST_QT_API: piecute, expected one of *"] + ["* Invalid value for $QT_API: piecute, expected one of *"] ) @@ -566,7 +566,7 @@ def _fake_import(name, *args): def _fake_is_library_loaded(name, *args): return False - monkeypatch.delenv("PYTEST_QT_API", raising=False) + monkeypatch.delenv("QT_API", raising=False) monkeypatch.setattr(qt_compat, "_import", _fake_import) monkeypatch.setattr(qt_compat, "_is_library_loaded", _fake_is_library_loaded) @@ -640,13 +640,13 @@ def _fake_import(name, *args, **kwargs): def _fake_is_library_loaded(name, *args): return name == backend - monkeypatch.delenv("PYTEST_QT_API", raising=False) + monkeypatch.delenv("QT_API", raising=False) monkeypatch.setattr(qt_compat, "_is_library_loaded", _fake_is_library_loaded) monkeypatch.setattr(builtins, "__import__", _fake_import) qt_api.set_qt_api(api=None) - assert qt_api.pytest_qt_api == option_api + assert qt_api.QT_API == option_api def test_before_close_func(testdir): diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 1a670ba5..64c96e28 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -114,7 +114,7 @@ def data( xfail_py311_pyside2 = pytest.mark.xfail( - sys.version_info[:2] == (3, 11) and qt_api.pytest_qt_api == "pyside2", + sys.version_info[:2] == (3, 11) and qt_api.QT_API == "pyside2", reason="Fails to OR mask flags", ) diff --git a/tests/test_wait_signal.py b/tests/test_wait_signal.py index 47cb749f..61992567 100644 --- a/tests/test_wait_signal.py +++ b/tests/test_wait_signal.py @@ -918,7 +918,7 @@ def test_empty_when_no_signal_name_available(self, qtbot, signaller): Tests that all_signals_and_args is empty even though expected signals are emitted, but signal names aren't available. """ - if qt_api.pytest_qt_api != "pyside2": + if qt_api.QT_API != "pyside2": pytest.skip( "test only makes sense for PySide2, whose signals don't contain a name!" ) @@ -1202,7 +1202,7 @@ def test_degenerate_error_msg(self, qtbot, signaller): by the user. This degenerate messages doesn't contain the signals' names, and includes a hint to the user how to fix the situation. """ - if qt_api.pytest_qt_api != "pyside2": + if qt_api.QT_API != "pyside2": pytest.skip( "test only makes sense for PySide, whose signals don't contain a name!" )