diff --git a/qtpy/QtCore.py b/qtpy/QtCore.py index 51791cf8..777db3b4 100644 --- a/qtpy/QtCore.py +++ b/qtpy/QtCore.py @@ -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_ @@ -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 diff --git a/qtpy/QtGui.py b/qtpy/QtGui.py index fcf6a13d..d355093e 100644 --- a/qtpy/QtGui.py +++ b/qtpy/QtGui.py @@ -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, @@ -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, diff --git a/qtpy/QtWidgets.py b/qtpy/QtWidgets.py index 5f8c251a..8e2ee8cb 100644 --- a/qtpy/QtWidgets.py +++ b/qtpy/QtWidgets.py @@ -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, @@ -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 @@ -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 @@ -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) @@ -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, diff --git a/qtpy/__init__.py b/qtpy/__init__.py index 697f23ae..a6bcc549 100644 --- a/qtpy/__init__.py +++ b/qtpy/__init__.py @@ -60,8 +60,6 @@ import sys import warnings -from packaging.version import parse - # Version of QtPy __version__ = "2.5.0.dev0" @@ -185,6 +183,30 @@ 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 into a comparable object""" + try: + from packaging.version import parse as _packaging_version_parse + except ImportError: + return _parse_version_internal(version) + else: + return _packaging_version_parse(version) + + +def _parse_version_internal(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: @@ -208,16 +230,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 " @@ -241,9 +267,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 " @@ -327,18 +355,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) diff --git a/qtpy/_utils.py b/qtpy/_utils.py index 5207b554..4b6ee5df 100644 --- a/qtpy/_utils.py +++ b/qtpy/_utils.py @@ -9,17 +9,17 @@ from functools import wraps from typing import TYPE_CHECKING -import qtpy +from . import QtModuleNotInstalledError if TYPE_CHECKING: - from qtpy.QtWidgets import QAction + from .QtWidgets import QAction def _wrap_missing_optional_dep_error( attr_error, *, import_error, - wrapper=qtpy.QtModuleNotInstalledError, + wrapper=QtModuleNotInstalledError, **wrapper_kwargs, ): """Create a __cause__-chained wrapper error for a missing optional dep.""" @@ -72,8 +72,8 @@ def possibly_static_exec_(cls, *args, **kwargs): def set_shortcut(self, shortcut, old_set_shortcut): """Ensure that the type of `shortcut` is compatible to `QAction.setShortcut`.""" - from qtpy.QtCore import Qt - from qtpy.QtGui import QKeySequence + from .QtCore import Qt + from .QtGui import QKeySequence if isinstance(shortcut, (QKeySequence.StandardKey, Qt.Key, int)): shortcut = QKeySequence(shortcut) @@ -82,8 +82,8 @@ def set_shortcut(self, shortcut, old_set_shortcut): def set_shortcuts(self, shortcuts, old_set_shortcuts): """Ensure that the type of `shortcuts` is compatible to `QAction.setShortcuts`.""" - from qtpy.QtCore import Qt - from qtpy.QtGui import QKeySequence + from .QtCore import Qt + from .QtGui import QKeySequence if isinstance( shortcuts, @@ -104,8 +104,8 @@ def set_shortcuts(self, shortcuts, old_set_shortcuts): def add_action(self, *args, old_add_action): """Re-order arguments of `addAction` to backport compatibility with Qt>=6.3.""" - from qtpy.QtCore import QObject, Qt - from qtpy.QtGui import QIcon, QKeySequence + from .QtCore import QObject, Qt + from .QtGui import QIcon, QKeySequence action: QAction icon: QIcon diff --git a/qtpy/tests/test_main.py b/qtpy/tests/test_main.py index 30af2b26..62ae8cef 100644 --- a/qtpy/tests/test_main.py +++ b/qtpy/tests/test_main.py @@ -5,7 +5,14 @@ import pytest -from qtpy import API_NAMES, QtCore, QtGui, QtWidgets +from qtpy import ( + API_NAMES, + QtCore, + QtGui, + QtWidgets, + _parse_version, + _parse_version_internal, +) from qtpy.tests.utils import pytest_importorskip with contextlib.suppress(Exception): @@ -141,3 +148,21 @@ def test_qt_api_environ(api): raise AssertionError('QtPy imported despite bad QT_API') """ subprocess.check_call([sys.executable, "-Oc", cmd], env=env) + + +@pytest.mark.parametrize( + "first,second", + [("1.2.3", "1.2.3.1"), ("1.2.3", "1.10.0")], +) +def test_parse_version(first, second): + """Verify the behavior of _parse_version()""" + assert _parse_version(first) < _parse_version(second) + + +@pytest.mark.parametrize( + "value,expect", + [("1.2.3", (1, 2, 3)), ("1.x.3", (1, 0, 3))], +) +def test_parse_version_internal(value, expect): + """Verify the behavior of _parse_version_internal()""" + assert _parse_version_internal(value) == expect diff --git a/qtpy/tests/test_qtconcurrent.py b/qtpy/tests/test_qtconcurrent.py index 8fc71777..ae7af817 100644 --- a/qtpy/tests/test_qtconcurrent.py +++ b/qtpy/tests/test_qtconcurrent.py @@ -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 @@ -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 diff --git a/qtpy/tests/test_qtcore.py b/qtpy/tests/test_qtcore.py index 9044290d..55fbb9e0 100644 --- a/qtpy/tests/test_qtcore.py +++ b/qtpy/tests/test_qtcore.py @@ -5,7 +5,6 @@ from datetime import date, datetime, time import pytest -from packaging.version import parse from qtpy import ( PYQT5, @@ -14,6 +13,7 @@ PYSIDE2, PYSIDE_VERSION, QtCore, + _parse_version, ) _now = datetime.now() @@ -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(): diff --git a/qtpy/tests/test_qtgui.py b/qtpy/tests/test_qtgui.py index 23fc0158..6b205553 100644 --- a/qtpy/tests/test_qtgui.py +++ b/qtpy/tests/test_qtgui.py @@ -3,7 +3,6 @@ import sys import pytest -from packaging.version import parse from qtpy import ( PYQT5, @@ -14,6 +13,7 @@ QtCore, QtGui, QtWidgets, + _parse_version, ) from qtpy.tests.utils import not_using_conda @@ -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( diff --git a/qtpy/tests/test_qttest.py b/qtpy/tests/test_qttest.py index 2d67439f..13b647f6 100644 --- a/qtpy/tests/test_qttest.py +++ b/qtpy/tests/test_qttest.py @@ -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(): @@ -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 ): diff --git a/qtpy/tests/test_qttexttospeech.py b/qtpy/tests/test_qttexttospeech.py index bcb97f09..ae82eeaf 100644 --- a/qtpy/tests/test_qttexttospeech.py +++ b/qtpy/tests/test_qttexttospeech.py @@ -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)", diff --git a/qtpy/tests/test_qtwebenginewidgets.py b/qtpy/tests/test_qtwebenginewidgets.py index 06339798..be12c435 100644 --- a/qtpy/tests/test_qtwebenginewidgets.py +++ b/qtpy/tests/test_qtwebenginewidgets.py @@ -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 ), diff --git a/qtpy/tests/test_uic.py b/qtpy/tests/test_uic.py index 94558461..ce25ef54 100644 --- a/qtpy/tests/test_uic.py +++ b/qtpy/tests/test_uic.py @@ -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 @@ -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.", ) diff --git a/qtpy/tests/utils.py b/qtpy/tests/utils.py index 68ac1668..0b09f2e3 100644 --- a/qtpy/tests/utils.py +++ b/qtpy/tests/utils.py @@ -3,7 +3,8 @@ import os import pytest -from packaging.version import parse + +from qtpy import _parse_version def using_conda(): @@ -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)