Skip to content

Commit

Permalink
Drop support for PySide2 (#583)
Browse files Browse the repository at this point in the history
PySide2 is no longer maintained, with the last release being made in 2022.
  • Loading branch information
nicoddemus authored Dec 12, 2024
1 parent 47cd6bf commit 2eda62b
Show file tree
Hide file tree
Showing 11 changed files with 31 additions and 84 deletions.
14 changes: 1 addition & 13 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,8 @@ jobs:

matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
qt-lib: [pyqt5, pyqt6, pyside2, pyside6]
qt-lib: [pyqt5, pyqt6, pyside6]
os: [ubuntu-latest, windows-latest, macos-latest]
exclude:
# Not installable:
# ERROR: Could not find a version that satisfies the requirement pyside2 (from versions: none)
- python-version: "3.11"
qt-lib: pyside2
os: windows-latest
- python-version: "3.12"
qt-lib: pyside2
- python-version: "3.13"
qt-lib: pyside2
- qt-lib: pyside2
os: macos-latest

steps:
- uses: actions/checkout@v4
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ UNRELEASED

* Added official support for Python 3.13.
* Dropped support for EOL Python 3.8.
* Dropped support for EOL PySide 2.

4.4.0 (2024-02-07)
------------------
Expand Down
12 changes: 5 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pytest-qt
=========

pytest-qt is a `pytest`_ plugin that allows programmers to write tests
for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PySide6`_ applications.
for `PyQt5`_, `PyQt6`_, and `PySide6`_ applications.

The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp``
creation as needed and provides methods to simulate user interaction,
Expand All @@ -22,7 +22,6 @@ like key presses and mouse clicks:
assert widget.greet_label.text() == "Hello!"
.. _PySide2: https://pypi.org/project/PySide2/
.. _PySide6: https://pypi.org/project/PySide6/
.. _PyQt5: https://pypi.org/project/PyQt5/
.. _PyQt6: https://pypi.org/project/PyQt6/
Expand Down Expand Up @@ -74,24 +73,23 @@ Features
Requirements
============

Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_.
Works with either PySide6_, PyQt6_ or PyQt5_.

If any of the above libraries is already imported by the time the tests execute, that library will be used.

If not, pytest-qt will try to import and use the Qt APIs, in this order:

- ``PySide6``
- ``PySide2``
- ``PyQt6``
- ``PyQt5``

To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to
``pyside6``, ``pyside2``, ``pyqt6`` or ``pyqt5``:
``pyside6``, ``pyqt6`` or ``pyqt5``:

.. code-block:: ini
[pytest]
qt_api=pyqt5
qt_api=pyqt6
Alternatively, you can set the ``PYTEST_QT_API`` environment
Expand Down Expand Up @@ -144,7 +142,7 @@ Running tests

Tests are run using `tox`_::

$ tox -e py37-pyside2,py37-pyqt5
$ tox -e py-pyside6,py-pyqt5

``pytest-qt`` is formatted using `black <https://github.com/ambv/black>`_ and uses
`pre-commit <https://github.com/pre-commit/pre-commit>`_ for linting checks before commits. You
Expand Down
8 changes: 3 additions & 5 deletions docs/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pytest-qt
=========

pytest-qt is a `pytest`_ plugin that allows programmers to write tests
for `PyQt5`_, `PyQt6`_, `PySide2`_ and `PySide6`_ applications.
for `PyQt5`_, `PyQt6`_, and `PySide6`_ applications.

The main usage is to use the ``qtbot`` fixture, responsible for handling ``qApp``
creation as needed, and registering widgets for testing:
Expand All @@ -21,7 +21,6 @@ creation as needed, and registering widgets for testing:
assert widget.greet_label.text() == "Hello!"
.. _PySide2: https://pypi.org/project/PySide2/
.. _PySide6: https://pypi.org/project/PySide6/
.. _PyQt5: https://pypi.org/project/PyQt5/
.. _PyQt6: https://pypi.org/project/PyQt6/
Expand Down Expand Up @@ -75,17 +74,16 @@ Requirements

``pytest-qt`` requires Python 3.7+.

Works with either PySide6_, PySide2_, PyQt6_ or PyQt5_, picking whichever
Works with either PySide6_, PyQt6_ or PyQt5_, picking whichever
is available on the system, giving preference to the first one installed in
this order:

- ``PySide6``
- ``PySide2``
- ``PyQt6``
- ``PyQt5``

To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to
``pyside6``, ``pyside2``, ``pyqt6`` or ``pyqt5``:
``pyside6``, ``pyqt6`` or ``pyqt5``:

.. code-block:: ini
Expand Down
4 changes: 1 addition & 3 deletions src/pytestqt/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,7 @@ def qtmodeltester(request):


def pytest_addoption(parser):
parser.addini(
"qt_api", 'Qt api version to use: "pyside6" , "pyside2", "pyqt6", "pyqt5"'
)
parser.addini("qt_api", 'Qt api version to use: "pyside6" , "pyqt6", "pyqt5"')
parser.addini("qt_no_exception_capture", "disable automatic exception capture")
parser.addini(
"qt_default_raising",
Expand Down
21 changes: 6 additions & 15 deletions src/pytestqt/qt_compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
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.
abstracting API differences between PyQt5/6 and PySide6.
.. 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.
Expand All @@ -19,7 +19,6 @@

QT_APIS = OrderedDict()
QT_APIS["pyside6"] = "PySide6"
QT_APIS["pyside2"] = "PySide2"
QT_APIS["pyqt6"] = "PyQt6"
QT_APIS["pyqt5"] = "PyQt5"

Expand Down Expand Up @@ -85,7 +84,7 @@ def set_qt_api(self, api):
or self._guess_qt_api()
)

self.is_pyside = self.pytest_qt_api in ["pyside2", "pyside6"]
self.is_pyside = self.pytest_qt_api in ["pyside6"]
self.is_pyqt = self.pytest_qt_api in ["pyqt5", "pyqt6"]

if not self.pytest_qt_api: # pragma: no cover
Expand All @@ -94,7 +93,7 @@ def set_qt_api(self, api):
for module, reason in sorted(self._import_errors.items())
)
msg = (
"pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n"
"pytest-qt requires either PySide6, PyQt5 or PyQt6 installed.\n"
+ errors
)
raise pytest.UsageError(msg)
Expand All @@ -112,7 +111,7 @@ def _import_module(module_name):

self._check_qt_api_version()

# qInfo is not exposed in PySide2/6 (#232)
# qInfo is not exposed in PySide6 (#232)
if hasattr(QtCore, "QMessageLogger"):
self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg)
elif hasattr(QtCore, "qInfo"):
Expand Down Expand Up @@ -151,8 +150,8 @@ def _check_qt_api_version(self):
)

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.
# exec was a keyword in Python 2, so PySide6 6.0
# names the corresponding method "exec_" instead.
#
# The old _exec() alias is removed in PyQt6 and also deprecated as of
# PySide 6.1:
Expand All @@ -170,14 +169,6 @@ def get_versions(self):
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",
Expand Down
8 changes: 3 additions & 5 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ def test_parse_ini_boolean_invalid():
pytestqt.qtbot._parse_ini_boolean("foo")


@pytest.mark.parametrize("option_api", ["pyqt5", "pyqt6", "pyside2", "pyside6"])
@pytest.mark.parametrize("option_api", ["pyqt5", "pyqt6", "pyside6"])
def test_qt_api_ini_config(testdir, monkeypatch, option_api):
"""
Test qt_api ini option handling.
Expand Down Expand Up @@ -479,7 +479,7 @@ def test_foo(qtbot):
result.stderr.fnmatch_lines(["*ModuleNotFoundError:*"])


@pytest.mark.parametrize("envvar", ["pyqt5", "pyqt6", "pyside2", "pyside6"])
@pytest.mark.parametrize("envvar", ["pyqt5", "pyqt6", "pyside6"])
def test_qt_api_ini_config_with_envvar(testdir, monkeypatch, envvar):
"""ensure environment variable wins over config value if both are present"""
testdir.makeini(
Expand Down Expand Up @@ -586,10 +586,9 @@ def _fake_is_library_loaded(name, *args):
monkeypatch.setattr(qt_compat, "_is_library_loaded", _fake_is_library_loaded)

expected = (
"pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n"
"pytest-qt requires either PySide6, PyQt5 or PyQt6 installed.\n"
" PyQt5.QtCore: Failed to import PyQt5.QtCore\n"
" PyQt6.QtCore: Failed to import PyQt6.QtCore\n"
" PySide2.QtCore: Failed to import PySide2.QtCore\n"
" PySide6.QtCore: Failed to import PySide6.QtCore"
)

Expand All @@ -602,7 +601,6 @@ def _fake_is_library_loaded(name, *args):
[
("pyqt5", "PyQt5"),
("pyqt6", "PyQt6"),
("pyside2", "PySide2"),
("pyside6", "PySide6"),
],
)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def test_qinfo(qtlog):
if qt_api.is_pyside:
assert (
qt_api.qInfo is None
), "pyside2/6 does not expose qInfo. If it does, update this test."
), "pyside6 does not expose qInfo. If it does, update this test."
return

qt_api.qInfo("this is an INFO message")
Expand Down
29 changes: 3 additions & 26 deletions tests/test_modeltest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import sys

import pytest

from pytestqt.qt_compat import qt_api
Expand Down Expand Up @@ -113,33 +111,12 @@ def data(
check_model(BrokenTypeModel(), should_pass=False)


def check_broken_flag_or():
flag = qt_api.QtCore.Qt.AlignmentFlag
try:
int(flag.AlignHorizontal_Mask | flag.AlignVertical_Mask)
except SystemError:
# Should not be happening anywhere else
assert sys.version_info[:2] == (3, 11) and qt_api.pytest_qt_api == "pyside2"
return True
return False


xfail_py311_pyside2 = pytest.mark.xfail(
check_broken_flag_or(),
reason="Fails to OR mask flags",
)


@pytest.mark.parametrize(
"role_value, should_pass",
[
pytest.param(
qt_api.QtCore.Qt.AlignmentFlag.AlignLeft, True, marks=xfail_py311_pyside2
),
pytest.param(
qt_api.QtCore.Qt.AlignmentFlag.AlignRight, True, marks=xfail_py311_pyside2
),
pytest.param(0xFFFFFF, False, marks=xfail_py311_pyside2),
pytest.param(qt_api.QtCore.Qt.AlignmentFlag.AlignLeft, True),
pytest.param(qt_api.QtCore.Qt.AlignmentFlag.AlignRight, True),
pytest.param(0xFFFFFF, False),
("foo", False),
(object(), False),
],
Expand Down
12 changes: 6 additions & 6 deletions tests/test_wait_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -881,7 +881,7 @@ def cb(str_param, int_param):
def get_mixed_signals_with_guaranteed_name(signaller):
"""
Returns a list of signals with the guarantee that the signals have names (i.e. the names are
manually provided in case of using PySide2, where the signal names cannot be determined at run-time).
manually provided in case of using PySide6, where the signal names cannot be determined at run-time).
"""
if qt_api.is_pyside:
signals = [
Expand Down Expand Up @@ -918,9 +918,9 @@ 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.pytest_qt_api != "pyside6":
pytest.skip(
"test only makes sense for PySide2, whose signals don't contain a name!"
"test only makes sense for PySide6, whose signals don't contain a name"
)

with qtbot.waitSignals(
Expand Down Expand Up @@ -1198,13 +1198,13 @@ def test_strict_order_violation(self, qtbot, signaller):

def test_degenerate_error_msg(self, qtbot, signaller):
"""
Tests that the TimeoutError message is degenerate when using PySide2 signals for which no name is provided
Tests that the TimeoutError message is degenerate when using PySide6 signals for which no name is provided
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.pytest_qt_api != "pyside6":
pytest.skip(
"test only makes sense for PySide, whose signals don't contain a name!"
"test only makes sense for PySide, whose signals don't contain a name"
)

with pytest.raises(TimeoutError) as excinfo:
Expand Down
4 changes: 1 addition & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
[tox]
envlist = py{39,310,311,312,313}-{pyqt5,pyside2,pyside6,pyqt6}
envlist = py{39,310,311,312,313}-{pyqt5,pyside6,pyqt6}

[testenv]
deps=
pytest
pyside6: pyside6
pyside2: pyside2
pyqt5: pyqt5
pyqt6: pyqt6
commands=
pytest --color=yes {posargs}
setenv=
pyside6: PYTEST_QT_API=pyside6
pyside2: PYTEST_QT_API=pyside2
pyqt5: PYTEST_QT_API=pyqt5
pyqt6: PYTEST_QT_API=pyqt6
QT_QPA_PLATFORM=offscreen
Expand Down

0 comments on commit 2eda62b

Please sign in to comment.