diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 73465177..691fb247 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -68,9 +68,9 @@ jobs: test-qt-minreqs: uses: pyapp-kit/workflows/.github/workflows/test-pyrepo.yml@v2 with: - python-version: "3.8" + python-version: "3.9" qt: pyqt5 - pip-post-installs: "qtpy==1.1.0 typing-extensions==3.7.4.3" + pip-post-installs: "qtpy==1.1.0 typing-extensions==4.5.0" # 4.5.0 is just for pint pip-install-flags: -e coverage-upload: artifact diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bd805ff8..e1fe4cb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ pytest All widgets must be well-tested, and should work on: -- Python 3.8 and above +- Python 3.9 and above - PyQt5 (5.11 and above) & PyQt6 - PySide2 (5.11 and above) & PySide6 - macOS, Windows, & Linux diff --git a/README.md b/README.md index 7426b94c..d028cc31 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ that are not provided in the native QtWidgets module. Components are tested on: - macOS, Windows, & Linux -- Python 3.8 and above +- Python 3.9 and above - PyQt5 (5.11 and above) & PyQt6 - PySide2 (5.11 and above) & PySide6 diff --git a/docs/index.md b/docs/index.md index d8be1181..60d24596 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ QtWidgets module. Components are tested on: - macOS, Windows, & Linux -- Python 3.8 and above +- Python 3.9 and above - PyQt5 (5.11 and above) & PyQt6 - PySide2 (5.11 and above) & PySide6 diff --git a/examples/throttler_demo.py b/examples/throttler_demo.py index 2b99b76b..f0dd4d3c 100644 --- a/examples/throttler_demo.py +++ b/examples/throttler_demo.py @@ -27,7 +27,7 @@ """ -from typing import Deque +from collections import deque from qtpy.QtCore import QRect, QSize, Qt, QTimer, Signal from qtpy.QtGui import QPainter, QPen @@ -65,8 +65,8 @@ def __init__(self, parent=None): self._scrollTimer.timeout.connect(self._scroll) self._scrollTimer.start() - self._signalActivations: Deque[int] = Deque() - self._throttledSignalActivations: Deque[int] = Deque() + self._signalActivations: deque[int] = deque() + self._throttledSignalActivations: deque[int] = deque() def sizeHint(self): return QSize(400, 200) @@ -84,7 +84,7 @@ def _scroll(self): self.update() - def scrollAndCut(self, v: Deque[int], cutoff: int): + def scrollAndCut(self, v: deque[int], cutoff: int): L = len(v) for p in range(L): v[p] += 1 @@ -121,7 +121,7 @@ def paintEvent(self, event): p.drawLine(0, h2, w, h2) p.restore() - def _drawSignals(self, p: QPainter, v: Deque[int], color, yStart, yEnd): + def _drawSignals(self, p: QPainter, v: deque[int], color, yStart, yEnd): p.save() pen = QPen() pen.setWidthF(2.0) diff --git a/pyproject.toml b/pyproject.toml index 92e6eccc..7ec10c39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" name = "superqt" description = "Missing widgets and components for PyQt/PySide" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.9" license = { text = "BSD 3-Clause License" } authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }] keywords = [ @@ -28,7 +28,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -41,13 +40,21 @@ dynamic = ["version"] dependencies = [ "pygments>=2.4.0", "qtpy>=1.1.0", - "typing-extensions >=3.7.4.3,!=3.10.0.0", + "typing-extensions >=3.7.4.3,!=3.10.0.0", # however, pint requires >4.5.0 ] # extras # https://peps.python.org/pep-0621/#dependencies-optional-dependencies [project.optional-dependencies] -test = ["pint", "pytest", "pytest-cov", "pytest-qt", "numpy", "cmap", "pyconify"] +test = [ + "pint", + "pytest", + "pytest-cov", + "pytest-qt", + "numpy", + "cmap", + "pyconify", +] dev = [ "ipython", "ruff", @@ -58,7 +65,13 @@ dev = [ "rich", "types-Pygments", ] -docs = ["mkdocs-macros-plugin", "mkdocs-material", "mkdocstrings[python]", "pint", "cmap"] +docs = [ + "mkdocs-macros-plugin", + "mkdocs-material", + "mkdocstrings[python]", + "pint", + "cmap", +] quantity = ["pint"] cmap = ["cmap >=0.1.1"] pyside2 = ["pyside2"] @@ -100,21 +113,30 @@ python = ["3.11"] [[tool.hatch.envs.test.matrix]] qt = ["pyside2", "pyqt5", "pyqt5.12"] -python = ["3.8"] +python = ["3.9"] [tool.hatch.envs.test.overrides] matrix.qt.extra-dependencies = [ - {value = "pyside2", if = ["pyside2"]}, - {value = "pyside6", if = ["pyside6"]}, - {value = "pyqt5", if = ["pyqt5"]}, - {value = "pyqt6", if = ["pyqt6"]}, - {value = "pyqt5==5.12", if = ["pyqt5.12"]}, + { value = "pyside2", if = [ + "pyside2", + ] }, + { value = "pyside6", if = [ + "pyside6", + ] }, + { value = "pyqt5", if = [ + "pyqt5", + ] }, + { value = "pyqt6", if = [ + "pyqt6", + ] }, + { value = "pyqt5==5.12", if = [ + "pyqt5.12", + ] }, ] -# https://github.com/charliermarsh/ruff [tool.ruff] line-length = 88 -target-version = "py38" +target-version = "py39" src = ["src", "tests"] # https://docs.astral.sh/ruff/rules @@ -132,7 +154,7 @@ select = [ "B", # flake8-bugbear "A001", # flake8-builtins "RUF", # ruff-specific rules - "TCH", # flake8-type-checking + "TC", # flake8-type-checking "TID", # flake8-tidy-imports ] ignore = [ @@ -159,6 +181,7 @@ filterwarnings = [ "ignore:QPixmapCache.find:DeprecationWarning:", "ignore:SelectableGroups dict interface:DeprecationWarning", "ignore:The distutils package is deprecated:DeprecationWarning", + "ignore:.*Skipping callback call set_result", ] # https://mypy.readthedocs.io/en/stable/config_file.html diff --git a/src/superqt/cmap/_catalog_combo.py b/src/superqt/cmap/_catalog_combo.py index dbbe088f..fdde476e 100644 --- a/src/superqt/cmap/_catalog_combo.py +++ b/src/superqt/cmap/_catalog_combo.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Container +from typing import TYPE_CHECKING from cmap import Colormap from qtpy.QtCore import Qt, Signal @@ -11,6 +11,8 @@ from ._cmap_utils import try_cast_colormap if TYPE_CHECKING: + from collections.abc import Container + from cmap._catalog import Category, Interpolation from qtpy.QtGui import QKeyEvent diff --git a/src/superqt/cmap/_cmap_combo.py b/src/superqt/cmap/_cmap_combo.py index aa9d78c1..a18ada71 100644 --- a/src/superqt/cmap/_cmap_combo.py +++ b/src/superqt/cmap/_cmap_combo.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Sequence +from typing import TYPE_CHECKING, Any from cmap import Colormap from qtpy.QtCore import Qt, Signal @@ -23,6 +23,8 @@ from ._cmap_utils import try_cast_colormap if TYPE_CHECKING: + from collections.abc import Sequence + from cmap._colormap import ColorStopsLike diff --git a/src/superqt/combobox/_color_combobox.py b/src/superqt/combobox/_color_combobox.py index cdf04933..dd835f48 100644 --- a/src/superqt/combobox/_color_combobox.py +++ b/src/superqt/combobox/_color_combobox.py @@ -3,7 +3,7 @@ import warnings from contextlib import suppress from enum import IntEnum, auto -from typing import Any, Literal, Sequence, cast +from typing import TYPE_CHECKING, Any, Literal, cast from qtpy.QtCore import QModelIndex, QPersistentModelIndex, QRect, QSize, Qt, Signal from qtpy.QtGui import QColor, QPainter @@ -19,6 +19,9 @@ from superqt.utils import signals_blocked +if TYPE_CHECKING: + from collections.abc import Sequence + _NAME_MAP = {QColor(x).name(): x for x in QColor.colorNames()} COLOR_ROLE = Qt.ItemDataRole.BackgroundRole diff --git a/src/superqt/combobox/_enum_combobox.py b/src/superqt/combobox/_enum_combobox.py index ea149aa5..9abf0e2e 100644 --- a/src/superqt/combobox/_enum_combobox.py +++ b/src/superqt/combobox/_enum_combobox.py @@ -3,7 +3,7 @@ from functools import reduce from itertools import combinations from operator import or_ -from typing import Optional, Tuple, TypeVar +from typing import Optional, TypeVar from qtpy.QtCore import Signal from qtpy.QtWidgets import QComboBox @@ -47,7 +47,7 @@ def _get_name(enum_value: Enum): return name -def _get_name_with_value(enum_value: Enum) -> Tuple[str, Enum]: +def _get_name_with_value(enum_value: Enum) -> tuple[str, Enum]: return _get_name(enum_value), enum_value diff --git a/src/superqt/elidable/_eliding.py b/src/superqt/elidable/_eliding.py index dedfcda0..2b508482 100644 --- a/src/superqt/elidable/_eliding.py +++ b/src/superqt/elidable/_eliding.py @@ -1,5 +1,3 @@ -from typing import List - from qtpy.QtCore import Qt from qtpy.QtGui import QFont, QFontMetrics, QTextLayout @@ -36,7 +34,7 @@ def setEllipsesWidth(self, width: int) -> None: self._ellipses_width = width @staticmethod - def wrapText(text, width, font=None) -> List[str]: + def wrapText(text, width, font=None) -> list[str]: """Returns `text`, split as it would be wrapped for `width`, given `font`. Static method. @@ -74,5 +72,5 @@ def _elidedText(self) -> str: # join them return "".join(text[:nlines] + [last_line]) - def _wrappedText(self) -> List[str]: + def _wrappedText(self) -> list[str]: return _GenericEliding.wrapText(self._text, self.width(), self.font()) diff --git a/src/superqt/fonticon/_iconfont.py b/src/superqt/fonticon/_iconfont.py index 639253fb..42de25a2 100644 --- a/src/superqt/fonticon/_iconfont.py +++ b/src/superqt/fonticon/_iconfont.py @@ -1,4 +1,5 @@ -from typing import Mapping, Type, Union +from collections.abc import Mapping +from typing import Union FONTFILE_ATTR = "__font_file__" @@ -69,7 +70,7 @@ class FA5S(IconFont): __font_file__ = "..." -def namespace2font(namespace: Union[Mapping, Type], name: str) -> Type[IconFont]: +def namespace2font(namespace: Union[Mapping, type], name: str) -> type[IconFont]: """Convenience to convert a namespace (class, module, dict) into an IconFont.""" if isinstance(namespace, type): if not isinstance(getattr(namespace, FONTFILE_ATTR), str): diff --git a/src/superqt/fonticon/_plugins.py b/src/superqt/fonticon/_plugins.py index 5a4a19f5..5f69c6fc 100644 --- a/src/superqt/fonticon/_plugins.py +++ b/src/superqt/fonticon/_plugins.py @@ -1,5 +1,5 @@ import contextlib -from typing import ClassVar, Dict, List, Set, Tuple +from typing import ClassVar from ._iconfont import IconFontMeta, namespace2font @@ -11,9 +11,9 @@ class FontIconManager: ENTRY_POINT: ClassVar[str] = "superqt.fonticon" - _PLUGINS: ClassVar[Dict[str, EntryPoint]] = {} - _LOADED: ClassVar[Dict[str, IconFontMeta]] = {} - _BLOCKED: ClassVar[Set[EntryPoint]] = set() + _PLUGINS: ClassVar[dict[str, EntryPoint]] = {} + _LOADED: ClassVar[dict[str, IconFontMeta]] = {} + _BLOCKED: ClassVar[set[EntryPoint]] = set() def _discover_fonts(self) -> None: self._PLUGINS.clear() @@ -86,15 +86,15 @@ def dict(self) -> dict: get_font_class = _manager._get_font_class -def discover() -> Tuple[str]: +def discover() -> tuple[str]: _manager._discover_fonts() -def available() -> Tuple[str]: +def available() -> tuple[str]: return tuple(_manager._PLUGINS) -def loaded(load_all=False) -> Dict[str, List[str]]: +def loaded(load_all=False) -> dict[str, list[str]]: if load_all: discover() for x in available(): diff --git a/src/superqt/fonticon/_qfont_icon.py b/src/superqt/fonticon/_qfont_icon.py index 675ec751..72f6bf11 100644 --- a/src/superqt/fonticon/_qfont_icon.py +++ b/src/superqt/fonticon/_qfont_icon.py @@ -2,9 +2,10 @@ import warnings from collections import abc, defaultdict +from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, ClassVar, DefaultDict, Sequence, Tuple, Union, cast +from typing import TYPE_CHECKING, ClassVar, Union, cast from qtpy import QT_VERSION from qtpy.QtCore import QObject, QPoint, QRect, QSize, Qt @@ -47,8 +48,8 @@ def __repr__(self) -> str: int, str, Qt.GlobalColor, - Tuple[int, int, int, int], - Tuple[int, int, int], + tuple[int, int, int, int], + tuple[int, int, int], None, ] @@ -159,7 +160,7 @@ class _QFontIconEngine(QIconEngine): def __init__(self, options: _IconOptions): super().__init__() self._opts: defaultdict[QIcon.State, dict[QIcon.Mode, _IconOptions | None]] = ( - DefaultDict(dict) + defaultdict(dict) ) self._opts[QIcon.State.Off][QIcon.Mode.Normal] = options self.update_hash() diff --git a/src/superqt/selection/_searchable_tree_widget.py b/src/superqt/selection/_searchable_tree_widget.py index 1cb8cdc7..22761068 100644 --- a/src/superqt/selection/_searchable_tree_widget.py +++ b/src/superqt/selection/_searchable_tree_widget.py @@ -1,5 +1,6 @@ import logging -from typing import Any, Iterable, Mapping +from collections.abc import Iterable, Mapping +from typing import Any from qtpy.QtCore import QRegularExpression from qtpy.QtWidgets import QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget diff --git a/src/superqt/sliders/_generic_range_slider.py b/src/superqt/sliders/_generic_range_slider.py index f1e9f063..d2999113 100644 --- a/src/superqt/sliders/_generic_range_slider.py +++ b/src/superqt/sliders/_generic_range_slider.py @@ -1,4 +1,5 @@ -from typing import List, Optional, Sequence, Tuple, TypeVar, Union +from collections.abc import Sequence +from typing import Optional, TypeVar, Union from qtpy import QtGui from qtpy.QtCore import Property, QEvent, QPoint, QPointF, QRect, QRectF, Qt, Signal @@ -42,11 +43,11 @@ def __init__(self, *args, **kwargs): self.valueChanged = self._valuesChanged self.sliderMoved = self._slidersMoved # list of values - self._value: List[_T] = [20, 80] + self._value: list[_T] = [20, 80] # list of current positions of each handle. same length as _value # If tracking is enabled (the default) this will be identical to _value - self._position: List[_T] = [20, 80] + self._position: list[_T] = [20, 80] # which handle is being pressed/hovered self._pressedIndex = 0 @@ -113,7 +114,7 @@ def applyMacStylePatch(self) -> None: # ############### QtOverrides ####################### - def value(self) -> Tuple[_T, ...]: + def value(self) -> tuple[_T, ...]: """Get current value of the widget as a tuple of integers.""" return tuple(self._value) @@ -332,7 +333,7 @@ def _setClickOffset(self, pos): # NOTE: this is very much tied to mousepress... not a generic "get control" def _getControlAtPos( self, pos: QPoint, opt: Optional[QStyleOptionSlider] = None - ) -> Tuple[QStyle.SubControl, int]: + ) -> tuple[QStyle.SubControl, int]: """Update self._pressedControl based on ev.pos().""" opt = opt or self._styleOption diff --git a/src/superqt/sliders/_labeled.py b/src/superqt/sliders/_labeled.py index cff7279e..61375401 100644 --- a/src/superqt/sliders/_labeled.py +++ b/src/superqt/sliders/_labeled.py @@ -3,7 +3,7 @@ import contextlib from enum import IntEnum, IntFlag, auto from functools import partial -from typing import Any, Iterable, overload +from typing import TYPE_CHECKING, Any, overload from qtpy import QtGui from qtpy.QtCore import Property, QPoint, QSize, Qt, Signal @@ -25,6 +25,9 @@ from ._sliders import QDoubleRangeSlider, QDoubleSlider, QRangeSlider +if TYPE_CHECKING: + from collections.abc import Iterable + class LabelPosition(IntEnum): NoLabel = 0 diff --git a/src/superqt/utils/_misc.py b/src/superqt/utils/_misc.py index ef4b33e1..b8e6b075 100644 --- a/src/superqt/utils/_misc.py +++ b/src/superqt/utils/_misc.py @@ -1,5 +1,6 @@ +from collections.abc import Iterator from contextlib import contextmanager -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING if TYPE_CHECKING: from qtpy.QtCore import QObject diff --git a/src/superqt/utils/_qthreading.py b/src/superqt/utils/_qthreading.py index c74da587..c433cb85 100644 --- a/src/superqt/utils/_qthreading.py +++ b/src/superqt/utils/_qthreading.py @@ -9,9 +9,7 @@ Any, Callable, ClassVar, - Generator, Generic, - Sequence, TypeVar, overload, ) @@ -19,6 +17,8 @@ from qtpy.QtCore import QObject, QRunnable, QThread, QThreadPool, QTimer, Signal if TYPE_CHECKING: + from collections.abc import Generator, Sequence + _T = TypeVar("_T") class SigInst(Generic[_T]): diff --git a/tests/test_searchable_tree.py b/tests/test_searchable_tree.py index c656a5f9..0d4663cb 100644 --- a/tests/test_searchable_tree.py +++ b/tests/test_searchable_tree.py @@ -1,5 +1,3 @@ -from typing import List, Tuple - import pytest from pytestqt.qtbot import QtBot from qtpy.QtCore import Qt @@ -30,15 +28,15 @@ def widget(qtbot: QtBot, data: dict) -> QSearchableTreeWidget: return widget -def columns(item: QTreeWidgetItem) -> Tuple[str, str]: +def columns(item: QTreeWidgetItem) -> tuple[str, str]: return item.text(0), item.text(1) -def all_items(tree: QTreeWidget) -> List[QTreeWidgetItem]: +def all_items(tree: QTreeWidget) -> list[QTreeWidgetItem]: return tree.findItems("", Qt.MatchContains | Qt.MatchRecursive) -def shown_items(tree: QTreeWidget) -> List[QTreeWidgetItem]: +def shown_items(tree: QTreeWidget) -> list[QTreeWidgetItem]: items = all_items(tree) return [item for item in items if not item.isHidden()] diff --git a/tests/zz_test_sliders/test_labeled_slider.py b/tests/zz_test_sliders/test_labeled_slider.py index feaac1ce..2a86bfe8 100644 --- a/tests/zz_test_sliders/test_labeled_slider.py +++ b/tests/zz_test_sliders/test_labeled_slider.py @@ -1,4 +1,5 @@ -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from unittest.mock import Mock import pytest diff --git a/tests/zz_test_sliders/test_range_slider.py b/tests/zz_test_sliders/test_range_slider.py index 29bc08d9..f2f9ea65 100644 --- a/tests/zz_test_sliders/test_range_slider.py +++ b/tests/zz_test_sliders/test_range_slider.py @@ -1,6 +1,7 @@ import math +from collections.abc import Iterable from itertools import product -from typing import Any, Iterable +from typing import Any from unittest.mock import Mock import pytest