Skip to content

Commit

Permalink
dependencies: sever the dependency on packaging.version
Browse files Browse the repository at this point in the history
Make qtpy more self-contained by implementing our own version `parse()`
function instead of relying on `packaging.version`.

This makes it `qtpy` completely independent so that it can be trivially
vendored and used in environments where `packaging` is not installed
on the user's machine.
  • Loading branch information
davvid committed Dec 18, 2024
1 parent fb130f1 commit 09caecd
Show file tree
Hide file tree
Showing 12 changed files with 80 additions and 52 deletions.
6 changes: 2 additions & 4 deletions qtpy/QtCore.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@
import contextlib
from typing import TYPE_CHECKING

from packaging.version import parse

from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version
from . import QT_VERSION as _qt_version
from ._utils import possibly_static_exec, possibly_static_exec_

Expand Down Expand Up @@ -159,7 +157,7 @@
)

# Passing as default value 0 in the same way PySide6 6.3.2 does for the `Qt.ItemFlags` definition.
if parse(_qt_version) > parse("6.3"):
if _parse_version(_qt_version) > _parse_version("6.3"):
Qt.ItemFlags = lambda value=0: Qt.ItemFlag(value)

# For issue #153 and updated for issue #305
Expand Down
6 changes: 2 additions & 4 deletions qtpy/QtGui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@

from functools import partialmethod

from packaging.version import parse

from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version
from . import QT_VERSION as _qt_version
from ._utils import (
getattr_missing_optional_dep,
Expand Down Expand Up @@ -265,7 +263,7 @@ def movePositionPatched(
QDropEvent.posF = lambda self: self.position()


if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):
if PYQT5 or PYSIDE2 or _parse_version(_qt_version) < _parse_version("6.4"):
# Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4
_action_set_shortcut = partialmethod(
set_shortcut,
Expand Down
12 changes: 5 additions & 7 deletions qtpy/QtWidgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
"""Provides widget classes and functions."""
from functools import partialmethod

from packaging.version import parse

from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6
from . import PYQT5, PYQT6, PYSIDE2, PYSIDE6, _parse_version
from . import QT_VERSION as _qt_version
from ._utils import (
add_action,
Expand Down Expand Up @@ -44,7 +42,7 @@ def __getattr__(name):
QUndoCommand,
)

if parse(_qt_version) < parse("6.4"):
if _parse_version(_qt_version) < _parse_version("6.4"):
# Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4
# See spyder-ide/qtpy#461
from qtpy.QtGui import QAction
Expand Down Expand Up @@ -120,7 +118,7 @@ def __getattr__(name):
elif PYSIDE6:
from PySide6.QtGui import QActionGroup, QShortcut, QUndoCommand

if parse(_qt_version) < parse("6.4"):
if _parse_version(_qt_version) < _parse_version("6.4"):
# Make `QAction.setShortcut` and `QAction.setShortcuts` compatible with Qt>=6.4
# See spyder-ide/qtpy#461
from qtpy.QtGui import QAction
Expand Down Expand Up @@ -175,7 +173,7 @@ def __getattr__(name):
)

# Passing as default value 0 in the same way PySide6 < 6.3.2 does for the `QFileDialog.Options` definition.
if parse(_qt_version) > parse("6.3"):
if _parse_version(_qt_version) > _parse_version("6.3"):
QFileDialog.Options = lambda value=0: QFileDialog.Option(value)


Expand Down Expand Up @@ -224,7 +222,7 @@ def __getattr__(name):
"directory",
)

if PYQT5 or PYSIDE2 or parse(_qt_version) < parse("6.4"):
if PYQT5 or PYSIDE2 or _parse_version(_qt_version) < _parse_version("6.4"):
# Make `addAction` compatible with Qt6 >= 6.4
_menu_add_action = partialmethod(
add_action,
Expand Down
58 changes: 43 additions & 15 deletions qtpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@
import sys
import warnings

from packaging.version import parse

# Version of QtPy
__version__ = "2.5.0.dev0"

Expand Down Expand Up @@ -185,6 +183,20 @@ def __init__(self, *, missing_package=None, **superclass_kwargs):
PYSIDE_VERSION = None
QT_VERSION = None


def _parse_int(value):
"""Convert a value into an integer"""
try:
return int(value)
except ValueError:
return 0


def _parse_version(version):
"""Parse a version string into a tuple of ints"""
return tuple(_parse_int(x) for x in version.split("."))


# Unless `FORCE_QT_API` is set, use previously imported Qt Python bindings
if not os.environ.get("FORCE_QT_API"):
if "PyQt5" in sys.modules:
Expand All @@ -208,16 +220,20 @@ def __init__(self, *, missing_package=None, **superclass_kwargs):
QT5 = PYQT5 = True

if sys.platform == "darwin":
macos_version = parse(platform.mac_ver()[0])
qt_ver = parse(QT_VERSION)
if macos_version < parse("10.10") and qt_ver >= parse("5.9"):
macos_version = _parse_version(platform.mac_ver()[0])
qt_ver = _parse_version(QT_VERSION)
if macos_version < _parse_version(
"10.10",
) and qt_ver >= _parse_version("5.9"):
raise PythonQtError(
"Qt 5.9 or higher only works in "
"macOS 10.10 or higher. Your "
"program will fail in this "
"system.",
)
elif macos_version < parse("10.11") and qt_ver >= parse("5.11"):
elif macos_version < _parse_version(
"10.11",
) and qt_ver >= _parse_version("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
"macOS 10.11 or higher. Your "
Expand All @@ -241,9 +257,11 @@ def __init__(self, *, missing_package=None, **superclass_kwargs):
QT5 = PYSIDE2 = True

if sys.platform == "darwin":
macos_version = parse(platform.mac_ver()[0])
qt_ver = parse(QT_VERSION)
if macos_version < parse("10.11") and qt_ver >= parse("5.11"):
macos_version = _parse_version(platform.mac_ver()[0])
qt_ver = _parse_version(QT_VERSION)
if macos_version < _parse_version(
"10.11",
) and qt_ver >= _parse_version("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
"macOS 10.11 or higher. Your "
Expand Down Expand Up @@ -327,18 +345,28 @@ def _warn_old_minor_version(name, old_version, min_version):

# Warn if using an End of Life or unsupported Qt API/binding minor version
if QT_VERSION:
if QT5 and (parse(QT_VERSION) < parse(QT5_VERSION_MIN)):
if QT5 and (_parse_version(QT_VERSION) < _parse_version(QT5_VERSION_MIN)):
_warn_old_minor_version("Qt5", QT_VERSION, QT5_VERSION_MIN)
elif QT6 and (parse(QT_VERSION) < parse(QT6_VERSION_MIN)):
elif QT6 and (
_parse_version(QT_VERSION) < _parse_version(QT6_VERSION_MIN)
):
_warn_old_minor_version("Qt6", QT_VERSION, QT6_VERSION_MIN)

if PYQT_VERSION:
if PYQT5 and (parse(PYQT_VERSION) < parse(PYQT5_VERSION_MIN)):
if PYQT5 and (
_parse_version(PYQT_VERSION) < _parse_version(PYQT5_VERSION_MIN)
):
_warn_old_minor_version("PyQt5", PYQT_VERSION, PYQT5_VERSION_MIN)
elif PYQT6 and (parse(PYQT_VERSION) < parse(PYQT6_VERSION_MIN)):
elif PYQT6 and (
_parse_version(PYQT_VERSION) < _parse_version(PYQT6_VERSION_MIN)
):
_warn_old_minor_version("PyQt6", PYQT_VERSION, PYQT6_VERSION_MIN)
elif PYSIDE_VERSION:
if PYSIDE2 and (parse(PYSIDE_VERSION) < parse(PYSIDE2_VERSION_MIN)):
if PYSIDE2 and (
_parse_version(PYSIDE_VERSION) < _parse_version(PYSIDE2_VERSION_MIN)
):
_warn_old_minor_version("PySide2", PYSIDE_VERSION, PYSIDE2_VERSION_MIN)
elif PYSIDE6 and (parse(PYSIDE_VERSION) < parse(PYSIDE6_VERSION_MIN)):
elif PYSIDE6 and (
_parse_version(PYSIDE_VERSION) < _parse_version(PYSIDE6_VERSION_MIN)
):
_warn_old_minor_version("PySide6", PYSIDE_VERSION, PYSIDE6_VERSION_MIN)
5 changes: 2 additions & 3 deletions qtpy/tests/test_qtconcurrent.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pytest
from packaging.version import parse

from qtpy import PYSIDE2, PYSIDE_VERSION
from qtpy import PYSIDE2, PYSIDE_VERSION, _parse_version
from qtpy.tests.utils import pytest_importorskip


Expand All @@ -11,7 +10,7 @@ def test_qtconcurrent():

assert QtConcurrent.QtConcurrent is not None

if PYSIDE2 and parse(PYSIDE_VERSION) >= parse("5.15.2"):
if PYSIDE2 and _parse_version(PYSIDE_VERSION) >= _parse_version("5.15.2"):
assert QtConcurrent.QFutureQString is not None
assert QtConcurrent.QFutureVoid is not None
assert QtConcurrent.QFutureWatcherQString is not None
Expand Down
4 changes: 2 additions & 2 deletions qtpy/tests/test_qtcore.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from datetime import date, datetime, time

import pytest
from packaging.version import parse

from qtpy import (
PYQT5,
Expand All @@ -14,6 +13,7 @@
PYSIDE2,
PYSIDE_VERSION,
QtCore,
_parse_version,
)

_now = datetime.now()
Expand Down Expand Up @@ -71,7 +71,7 @@ def test_qthread_exec():


@pytest.mark.skipif(
PYSIDE2 and parse(PYSIDE_VERSION) < parse("5.15"),
PYSIDE2 and _parse_version(PYSIDE_VERSION) < _parse_version("5.15"),
reason="QEnum macro doesn't seem to be present on PySide2 <5.15",
)
def test_qenum():
Expand Down
4 changes: 2 additions & 2 deletions qtpy/tests/test_qtgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import sys

import pytest
from packaging.version import parse

from qtpy import (
PYQT5,
Expand All @@ -14,6 +13,7 @@
QtCore,
QtGui,
QtWidgets,
_parse_version,
)
from qtpy.tests.utils import not_using_conda

Expand Down Expand Up @@ -198,7 +198,7 @@ def test_QAction_functions(qtbot):


@pytest.mark.skipif(
parse(QT_VERSION) < parse("6.5.0"),
_parse_version(QT_VERSION) < _parse_version("6.5.0"),
reason="Qt6 >= 6.5 specific test",
)
@pytest.mark.skipif(
Expand Down
5 changes: 2 additions & 3 deletions qtpy/tests/test_qttest.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import pytest
from packaging import version

from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE6, QtTest
from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE6, QtTest, _parse_version


def test_qttest():
Expand All @@ -12,7 +11,7 @@ def test_qttest():
assert QtTest.QSignalSpy is not None

if (
(PYQT5 and version.parse(PYQT_VERSION) >= version.parse("5.11"))
(PYQT5 and _parse_version(PYQT_VERSION) >= _parse_version("5.11"))
or PYQT6
or PYSIDE6
):
Expand Down
5 changes: 2 additions & 3 deletions qtpy/tests/test_qttexttospeech.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import pytest
from packaging import version

from qtpy import PYQT5, PYQT_VERSION, PYSIDE2
from qtpy import PYQT5, PYQT_VERSION, PYSIDE2, _parse_version


@pytest.mark.skipif(
not (
(PYQT5 and version.parse(PYQT_VERSION) >= version.parse("5.15.1"))
(PYQT5 and _parse_version(PYQT_VERSION) >= _parse_version("5.15.1"))
or PYSIDE2
),
reason="Only available in Qt5 bindings (PyQt5 >= 5.15.1 or PySide2)",
Expand Down
17 changes: 13 additions & 4 deletions qtpy/tests/test_qtwebenginewidgets.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import pytest
from packaging import version

from qtpy import PYQT5, PYQT6, PYQT_VERSION, PYSIDE2, PYSIDE6, PYSIDE_VERSION
from qtpy import (
PYQT5,
PYQT6,
PYQT_VERSION,
PYSIDE2,
PYSIDE6,
PYSIDE_VERSION,
_parse_version,
)
from qtpy.tests.utils import pytest_importorskip


@pytest.mark.skipif(
not (
(PYQT6 and version.parse(PYQT_VERSION) >= version.parse("6.2"))
or (PYSIDE6 and version.parse(PYSIDE_VERSION) >= version.parse("6.2"))
(PYQT6 and _parse_version(PYQT_VERSION) >= _parse_version("6.2"))
or (
PYSIDE6 and _parse_version(PYSIDE_VERSION) >= _parse_version("6.2")
)
or PYQT5
or PYSIDE2
),
Expand Down
5 changes: 2 additions & 3 deletions qtpy/tests/test_uic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
import warnings

import pytest
from packaging.version import parse

from qtpy import PYSIDE2, PYSIDE6, PYSIDE_VERSION, QtWidgets
from qtpy import PYSIDE2, PYSIDE6, PYSIDE_VERSION, QtWidgets, _parse_version
from qtpy.QtWidgets import QComboBox
from qtpy.tests.utils import pytest_importorskip, using_conda

Expand Down Expand Up @@ -63,7 +62,7 @@ def test_load_ui(qtbot):
@pytest.mark.skipif(
PYSIDE6
and using_conda()
and parse(PYSIDE_VERSION) < parse("6.5")
and _parse_version(PYSIDE_VERSION) < _parse_version("6.5")
and (sys.platform in ("darwin", "linux")),
reason="pyside6-uic command not contained in all conda-forge packages.",
)
Expand Down
5 changes: 3 additions & 2 deletions qtpy/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import os

import pytest
from packaging.version import parse

from qtpy import _parse_version


def using_conda():
Expand All @@ -22,6 +23,6 @@ def pytest_importorskip(module, **kwargs):
Python 3.7+. The `exc_type` argument was added in `pytest` 8.2.0.
See spyder-ide/qtpy#485
"""
if parse(pytest.__version__) < parse("8.2.0"):
if _parse_version(pytest.__version__) < _parse_version("8.2.0"):
return pytest.importorskip(module, **kwargs)
return pytest.importorskip(module, **kwargs, exc_type=ImportError)

0 comments on commit 09caecd

Please sign in to comment.