From 2ca01a91392684aed4f7599f103ddcad9c9f4bfe Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Wed, 26 Aug 2026 14:31:05 +0200 Subject: [PATCH] feat!: drop python 3.9 and 3.10, require >=3.11 BREAKING CHANGE: python 3.9 and 3.10 are no longer supported, and the PySide2 backend is dropped along with them (PySide2 5.15.2.1 declares `requires_python <3.11` and ships no wheel past cp310, so it cannot be installed or tested on the new floor). - requires-python = ">=3.11"; drop the 3.9/3.10 classifiers - drop the pyside2 extra, dependency-group, docs and CI jobs, along with the test-qt-legacy group and [tool.uv] conflicts block that existed only to keep PySide2 on pytest-qt 4.4 - CI: base matrix 3.10 -> 3.11, min-deps 3.9 -> 3.11, pyqt5 job 3.9 -> 3.11, test-dependents default 3.10 -> 3.11 - SLOTS is now unconditional, and the <3.11 weakref_slot skip is dead - ruff target-version py39 -> py311, and pin the dev ruff/mypy to the versions pre-commit already runs (they had drifted to ruff 0.15.12 against pre-commit's 0.16.4) Co-Authored-By: Claude Opus 5 --- .github/workflows/test_and_deploy.yml | 14 +++----- README.md | 8 ++--- docs/index.md | 3 +- docs/installation.md | 1 - pyproject.toml | 35 ++++++++++--------- src/magicgui/_type_resolution.py | 3 +- src/magicgui/_util.py | 10 ++++-- src/magicgui/application.py | 3 +- src/magicgui/backends/_ipynb/widgets.py | 3 +- src/magicgui/backends/_qtpy/widgets.py | 3 +- src/magicgui/schema/_guiclass.py | 7 ++-- src/magicgui/schema/_ui_field.py | 9 ++--- src/magicgui/signature.py | 5 ++- src/magicgui/type_map/_magicgui.py | 3 +- src/magicgui/type_map/_type_map.py | 9 ++--- src/magicgui/types.py | 4 +-- src/magicgui/widgets/_concrete.py | 12 ++++--- src/magicgui/widgets/_dialogs.py | 3 +- src/magicgui/widgets/_function_gui.py | 2 +- src/magicgui/widgets/_table.py | 12 ++++--- src/magicgui/widgets/bases/_button_widget.py | 3 +- .../widgets/bases/_categorical_widget.py | 3 +- .../widgets/bases/_container_widget.py | 3 +- src/magicgui/widgets/bases/_ranged_widget.py | 4 +-- src/magicgui/widgets/bases/_slider_widget.py | 3 +- src/magicgui/widgets/bases/_toolbar.py | 3 +- src/magicgui/widgets/bases/_value_widget.py | 13 +++++-- src/magicgui/widgets/protocols.py | 2 +- tests/test_backends.py | 4 +-- tests/test_gui_class.py | 4 --- tests/test_table.py | 2 +- tests/test_types.py | 3 +- tests/test_widgets.py | 4 +-- 33 files changed, 105 insertions(+), 95 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index ec205bd13..77ac091e5 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -23,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.13"] + python-version: ["3.11", "3.13"] os: [ubuntu-latest, macos-latest, windows-latest] add-group: [pyqt6, pyside6] include: @@ -31,18 +31,12 @@ jobs: python-version: "3.11" resolution: "lowest-direct" add-group: pyside6 - - python-version: "3.9" + - python-version: "3.11" os: ubuntu-latest add-group: pyqt5 - - python-version: "3.9" - os: ubuntu-latest - add-group: pyside2 - python-version: "3.12" os: windows-latest add-group: pyqt5 - - python-version: "3.10" - os: windows-latest - add-group: pyside2 - python-version: "3.12" os: ubuntu-latest add-group: pyqt6 @@ -109,7 +103,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.13"] + python-version: ["3.11", "3.13"] os: [macos-latest] steps: @@ -148,7 +142,7 @@ jobs: dependency-extras: ${{ matrix.package-extras || '' }} dependency-group: ${{ matrix.package-group || '' }} qt: pyqt5 - python-version: ${{ matrix.python-version || '3.10' }} + python-version: ${{ matrix.python-version || '3.11' }} post-install-cmd: "python -m pip install pytest-pretty lxml_html_clean" # just for napari pytest-args: ${{ matrix.pytest-args }} strategy: diff --git a/README.md b/README.md index 2f862fb17..818b2de2e 100644 --- a/README.md +++ b/README.md @@ -39,21 +39,21 @@ ## Installation -`magicgui` uses `qtpy` to support both `pyside2` and `pyqt5` backends. However, you +`magicgui` uses `qtpy` to support both `pyqt` and `pyside` backends. However, you must have one of those installed for magicgui to work. install with pip ```bash -pip install magicgui[pyqt5] +pip install magicgui[pyqt6] # or -pip install magicgui[pyside2] +pip install magicgui[pyside6] ``` or with conda: ```bash -conda install -c conda-forge magicgui pyqt # or pyside2 instead of pyqt +conda install -c conda-forge magicgui pyqt # or pyside6 instead of pyqt ``` > :information_source: If you'd like to help us extend support to a different backend, diff --git a/docs/index.md b/docs/index.md index 43b914831..1c848860c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -160,8 +160,7 @@ environments (such as a desktop app, or a Jupyter notebook). Currently, **magicgui** supports the following backends: - [Qt](https://www.qt.io/) (via - [PySide2](https://pypi.org/project/PySide2/)/[PySide6](https://pypi.org/project/PySide6/) - or + [PySide6](https://pypi.org/project/PySide6/) or [PyQt5](https://pypi.org/project/PyQt5/)/[PyQt6](https://pypi.org/project/PyQt6/)) - [Jupyter Widgets](https://ipywidgets.readthedocs.io/en/latest/) (a.k.a. "IPyWidgets") diff --git a/docs/installation.md b/docs/installation.md index de4602db8..d61b15904 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -20,7 +20,6 @@ available: - `PyQt5`: `pip install magicgui[pyqt5]` - `PyQt6`: `pip install magicgui[pyqt6]` -- `PySide2`: `pip install magicgui[pyside2]` - `PySide6`: `pip install magicgui[pyside6]` - `Jupyter Widgets`: `pip install magicgui[jupyter]` diff --git a/pyproject.toml b/pyproject.toml index 7d879984f..ec012096c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ name = "magicgui" description = " build GUIs from python types" keywords = ["gui", "widgets", "type annotations"] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" license = { text = "MIT" } authors = [{ email = "talley.lambert@gmail.com", name = "Talley Lambert" }] classifiers = [ @@ -32,8 +32,6 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -63,7 +61,6 @@ pyqt5 = [ "pyqt5-qt5>5.15.2; sys_platform != 'win32'", ] pyqt6 = ["pyqt6>=6.4.0"] -pyside2 = ["pyside2>=5.15"] pyside6 = ["pyside6>=6.4.0"] tqdm = ["tqdm>=4.30.0"] jupyter = ["ipywidgets>=8.0.0"] @@ -78,17 +75,14 @@ third-party-support = [ "matplotlib>=3.9.4", "numpy>=2.1.0; python_version >= '3.13'", "numpy>=1.26.4", - "pandas>=2.2.3; python_version >= '3.11'", - "pandas>=2.1", + "pandas>=2.2.3", "pydantic>=1.10.18", "toolz>=1.0.0", ] test-min = ["pytest>=8.4.0", "pytest-cov >=6.1", "pytest-mypy-plugins>=3.1"] # pytest-qt >=4.5 fixes SystemError/TimeoutError in qtbot.waitSignal with -# recent PySide6 (pytest-dev/pytest-qt#552), but drops PySide2 support -- -# so the pyside2 group uses the legacy pin (see [tool.uv] conflicts below) +# recent PySide6 (pytest-dev/pytest-qt#552) test-qt = [{ include-group = "test-min" }, "pytest-qt>=4.5.0"] -test-qt-legacy = [{ include-group = "test-min" }, "pytest-qt==4.4.0"] test = [ "magicgui[tqdm,jupyter,image,quantity]", { include-group = "test-min" }, @@ -96,13 +90,12 @@ test = [ ] pyqt5 = ["magicgui[pyqt5]", { include-group = "test-qt" }] pyqt6 = ["magicgui[pyqt6]", { include-group = "test-qt" }] -pyside2 = ["magicgui[pyside2]", { include-group = "test-qt-legacy" }, "numpy<2; python_version < '3.13'"] pyside6 = ["magicgui[pyside6]", { include-group = "test-qt" }] dev = [ { include-group = "test" }, - "ruff>=0.8.3", + "ruff>=0.16.4", "ipython>=8.18.0", - "mypy>=1.13.0", + "mypy>=2.3.1", "pdbpp>=0.11.6; sys_platform != 'win32'", "pre-commit-uv>=4", "pyqt6>=6.8.0", @@ -129,10 +122,6 @@ docs = [ "ipykernel>=6.29.5", ] -[tool.uv] -# the two pytest-qt pins are mutually exclusive; no environment installs both -conflicts = [[{ group = "test-qt" }, { group = "test-qt-legacy" }]] - [tool.uv.sources] magicgui = { workspace = true } @@ -148,7 +137,7 @@ documentation = "https://pyapp-kit.github.io/magicgui/" # https://docs.astral.sh/ruff [tool.ruff] line-length = 88 -target-version = "py39" +target-version = "py311" src = ["src", "tests"] fix = true unsafe-fixes = true @@ -172,8 +161,20 @@ select = [ ] ignore = [ "D401", # First line should be in imperative mood + # magicgui resolves annotations at *runtime*, and on python < 3.14 + # `get_origin(int | None)` is `types.UnionType`, not `typing.Union` -- so + # rewriting Optional/Union to PEP 604 changes behaviour rather than just + # spelling. Tests also deliberately exercise both spellings. + "UP007", # Use `X | Y` for type annotations + "UP045", # Use `X | None` for type annotations ] +[tool.ruff.lint.flake8-type-checking] +# ruff exempts `typing`/`typing_extensions` by default; `collections.abc` now +# supplies Callable etc. after the pyupgrade rewrite, and magicgui resolves +# annotations at runtime -- so these names must stay importable at runtime. +exempt-modules = ["typing", "typing_extensions", "collections.abc"] + [tool.ruff.lint.per-file-ignores] "tests/*.py" = ["D", "S", "E501"] "tests/test_util.py" = ["D", "S", "E501", "UP006"] diff --git a/src/magicgui/_type_resolution.py b/src/magicgui/_type_resolution.py index e7df55414..e3d0137b4 100644 --- a/src/magicgui/_type_resolution.py +++ b/src/magicgui/_type_resolution.py @@ -1,9 +1,10 @@ import types import typing +from collections.abc import Callable from copy import copy from functools import lru_cache, partial from importlib import import_module -from typing import Any, Callable, Optional, Union, get_type_hints +from typing import Any, Optional, Union, get_type_hints try: from toolz import curry diff --git a/src/magicgui/_util.py b/src/magicgui/_util.py index a6054d1dd..5c9fcfa72 100644 --- a/src/magicgui/_util.py +++ b/src/magicgui/_util.py @@ -4,11 +4,11 @@ import os import sys import time +from collections.abc import Callable from functools import wraps from pathlib import Path from typing import ( TYPE_CHECKING, - Callable, get_args, get_origin, overload, @@ -177,7 +177,8 @@ def _safe_isinstance_tuple(obj: object, superclass: object) -> bool: return all(safe_issubclass(o, superclass_args[0]) for o in obj_args) # fallback to simple compare return len(obj_args) == len(superclass_args) and all( - safe_issubclass(o, s) for o, s in zip(obj_args, superclass_args) + safe_issubclass(o, s) + for o, s in zip(obj_args, superclass_args, strict=False) ) if len(obj_args) == 2 and obj_args[1] is Ellipsis: @@ -215,7 +216,10 @@ def safe_issubclass(obj: object, superclass: object) -> bool: return True if len(obj_args) != len(superclass_args): return False - return all(safe_issubclass(o, s) for o, s in zip(obj_args, superclass_args)) + return all( + safe_issubclass(o, s) + for o, s in zip(obj_args, superclass_args, strict=False) + ) except Exception: return False diff --git a/src/magicgui/application.py b/src/magicgui/application.py index 51aecb890..fc8e8ac92 100644 --- a/src/magicgui/application.py +++ b/src/magicgui/application.py @@ -3,9 +3,10 @@ from __future__ import annotations import signal +from collections.abc import Callable from contextlib import contextmanager from importlib import import_module -from typing import TYPE_CHECKING, Any, Callable, Union +from typing import TYPE_CHECKING, Any, Union from magicgui.backends import BACKENDS diff --git a/src/magicgui/backends/_ipynb/widgets.py b/src/magicgui/backends/_ipynb/widgets.py index f5f56c38f..4859d022d 100644 --- a/src/magicgui/backends/_ipynb/widgets.py +++ b/src/magicgui/backends/_ipynb/widgets.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, get_type_hints +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, get_type_hints try: import ipywidgets diff --git a/src/magicgui/backends/_qtpy/widgets.py b/src/magicgui/backends/_qtpy/widgets.py index f6979a8d3..8dfb940c9 100644 --- a/src/magicgui/backends/_qtpy/widgets.py +++ b/src/magicgui/backends/_qtpy/widgets.py @@ -5,10 +5,11 @@ import math import re import warnings +from collections.abc import Callable from contextlib import contextmanager, suppress from functools import partial from itertools import chain -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any import qtpy import superqt diff --git a/src/magicgui/schema/_guiclass.py b/src/magicgui/schema/_guiclass.py index bd2ca1570..5a5264af2 100644 --- a/src/magicgui/schema/_guiclass.py +++ b/src/magicgui/schema/_guiclass.py @@ -11,8 +11,9 @@ import contextlib import warnings +from collections.abc import Callable from dataclasses import Field, dataclass, field, is_dataclass -from typing import TYPE_CHECKING, Any, Callable, ClassVar, TypeVar, overload +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload from psygnal import SignalGroup, SignalInstance, evented, is_evented from psygnal import __version__ as psygnal_version @@ -23,9 +24,7 @@ if TYPE_CHECKING: from collections.abc import Mapping - from typing import Protocol - - from typing_extensions import TypeGuard + from typing import Protocol, TypeGuard # fmt: off class GuiClassProtocol(Protocol): diff --git a/src/magicgui/schema/_ui_field.py b/src/magicgui/schema/_ui_field.py index 40039a18b..0ab019212 100644 --- a/src/magicgui/schema/_ui_field.py +++ b/src/magicgui/schema/_ui_field.py @@ -3,6 +3,7 @@ import dataclasses as dc import sys import warnings +from collections.abc import Callable from dataclasses import dataclass, field from functools import cache from types import FunctionType @@ -10,16 +11,16 @@ TYPE_CHECKING, Annotated, Any, - Callable, Generic, Literal, + TypeGuard, TypeVar, Union, cast, + get_args, + get_origin, ) -from typing_extensions import TypeGuard, get_args, get_origin - from magicgui.types import JsonStringFormats, Undefined, _Undefined if TYPE_CHECKING: @@ -42,7 +43,7 @@ class HasAttrs(Protocol): __all__ = ["UiField", "build_widget", "get_ui_fields"] -SLOTS = {"slots": True} if sys.version_info >= (3, 10) else {} +SLOTS = {"slots": True} T = TypeVar("T") diff --git a/src/magicgui/signature.py b/src/magicgui/signature.py index 3a84cb857..a37f98d6b 100644 --- a/src/magicgui/signature.py +++ b/src/magicgui/signature.py @@ -17,10 +17,9 @@ import inspect import typing import warnings +from collections.abc import Callable from types import MappingProxyType -from typing import TYPE_CHECKING, Annotated, Any, Callable, cast - -from typing_extensions import get_args, get_origin +from typing import TYPE_CHECKING, Annotated, Any, cast, get_args, get_origin from magicgui.types import Undefined diff --git a/src/magicgui/type_map/_magicgui.py b/src/magicgui/type_map/_magicgui.py index 4007df0bb..83db9cf8c 100644 --- a/src/magicgui/type_map/_magicgui.py +++ b/src/magicgui/type_map/_magicgui.py @@ -1,8 +1,9 @@ from __future__ import annotations import inspect +from collections.abc import Callable from functools import partial -from typing import Any, Callable, Generic, TypeVar +from typing import Any, Generic, TypeVar from magicgui.type_map._type_map import TypeMap from magicgui.widgets import FunctionGui diff --git a/src/magicgui/type_map/_type_map.py b/src/magicgui/type_map/_type_map.py index 1327ac59f..62f2d35ff 100644 --- a/src/magicgui/type_map/_type_map.py +++ b/src/magicgui/type_map/_type_map.py @@ -11,23 +11,24 @@ import types import warnings from collections import defaultdict -from collections.abc import Iterator, Sequence +from collections.abc import Callable, Iterator, Sequence from contextlib import contextmanager from enum import EnumMeta from typing import ( TYPE_CHECKING, Annotated, Any, - Callable, ForwardRef, Literal, TypeVar, Union, cast, + get_args, + get_origin, overload, ) -from typing_extensions import ParamSpec, get_args, get_origin +from typing_extensions import ParamSpec from magicgui import widgets from magicgui._type_resolution import resolve_single_type @@ -149,7 +150,7 @@ def match_type(self, type_: Any, default: Any | None = None) -> WidgetTuple | No if safe_issubclass(origin, set): for arg in get_args(type_): - if get_origin(arg) is Literal: # type: ignore [comparison-overlap] + if get_origin(arg) is Literal: return widgets.Select, {"choices": get_args(arg)} pint = sys.modules.get("pint") diff --git a/src/magicgui/types.py b/src/magicgui/types.py index 9ebfdab9f..b549cce35 100644 --- a/src/magicgui/types.py +++ b/src/magicgui/types.py @@ -2,10 +2,10 @@ from __future__ import annotations -from collections.abc import Iterable +from collections.abc import Callable, Iterable from enum import Enum, EnumMeta from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, Literal, Union +from typing import TYPE_CHECKING, Any, Literal, Union from typing_extensions import TypedDict diff --git a/src/magicgui/widgets/_concrete.py b/src/magicgui/widgets/_concrete.py index 07062dd34..f6847b629 100644 --- a/src/magicgui/widgets/_concrete.py +++ b/src/magicgui/widgets/_concrete.py @@ -11,24 +11,24 @@ import inspect import math import os +from collections.abc import Callable from pathlib import Path from typing import ( TYPE_CHECKING, Annotated, Any, - Callable, ForwardRef, Generic, Literal, TypeVar, Union, cast, + get_args, + get_origin, overload, ) from weakref import ref -from typing_extensions import get_args, get_origin - from magicgui._type_resolution import resolve_single_type from magicgui._util import merge_super_sigs, safe_issubclass from magicgui.application import use_app @@ -860,7 +860,9 @@ def __setitem__(self, key: int | slice, value: _V | Iterable[_V]) -> None: value_list = list(value) # type: ignore if len(value_list) != len(self._obj._get_child_widgets(key)): raise ValueError("Length of value does not match.") - for w, v in zip(self._obj._get_child_widgets(key), value_list): + for w, v in zip( + self._obj._get_child_widgets(key), value_list, strict=False + ): w.value = v self._obj.changed.emit(self._obj.value) else: @@ -989,7 +991,7 @@ def set_value(self, vals: Sequence[Any]) -> None: raise ValueError("Length of tuple does not match.") with self.changed.blocked(): - for w, v in zip(self._list, vals): + for w, v in zip(self._list, vals, strict=False): w.value = v # type: ignore self.changed.emit(self.value) diff --git a/src/magicgui/widgets/_dialogs.py b/src/magicgui/widgets/_dialogs.py index b0cb9867d..50b3e8dc2 100644 --- a/src/magicgui/widgets/_dialogs.py +++ b/src/magicgui/widgets/_dialogs.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING, Any from magicgui.types import FileDialogMode diff --git a/src/magicgui/widgets/_function_gui.py b/src/magicgui/widgets/_function_gui.py index 90308f610..838d9d8df 100644 --- a/src/magicgui/widgets/_function_gui.py +++ b/src/magicgui/widgets/_function_gui.py @@ -8,12 +8,12 @@ import inspect import re from collections import deque +from collections.abc import Callable from contextlib import contextmanager from types import FunctionType from typing import ( TYPE_CHECKING, Any, - Callable, Generic, NoReturn, TypeVar, diff --git a/src/magicgui/widgets/_table.py b/src/magicgui/widgets/_table.py index 020a36395..9e05dd3da 100644 --- a/src/magicgui/widgets/_table.py +++ b/src/magicgui/widgets/_table.py @@ -30,9 +30,11 @@ from magicgui.widgets.bases._value_widget import ValueWidget if TYPE_CHECKING: + from typing import TypeGuard + import numpy import pandas - from typing_extensions import TypeGuard, Unpack + from typing_extensions import Unpack from magicgui.widgets.protocols import TableWidgetProtocol @@ -502,7 +504,7 @@ def _set_row(self, row: TblKey, value: Collection, cols: slice = SliceNone) -> N def _set_rowi(self, row: int, value: Collection, cols: slice = SliceNone) -> None: """Set row by row index.""" self._assert_row(row) - for v, col in zip(value, self._iter_slice(cols, 1)): + for v, col in zip(value, self._iter_slice(cols, 1), strict=False): self._set_cell(row, col, v) def _assert_row(self, row: int) -> int: @@ -697,7 +699,7 @@ def __setitem__( if c_idx.step and c_idx.step != 1: # TODO: check value is iterable self._assert_extended_slice(c_idx, len(value[0]), axis=1) - for v, r in zip(value, obj._iter_slice(r_idx, 0)): + for v, r in zip(value, obj._iter_slice(r_idx, 0), strict=False): obj._set_rowi(r, v, c_idx) return raise ValueError(f"Not a valid idx for __setitem__ {idx!r}") @@ -791,7 +793,7 @@ def _from_nested_column_dict(data: dict) -> tuple[list[list], list]: break new_data = [[s[i] for i in index] for s in data.values()] - return [list(x) for x in zip(*new_data)], index + return [list(x) for x in zip(*new_data, strict=False)], index def _from_dict(data: dict) -> tuple[list[list], list, list]: @@ -806,7 +808,7 @@ def _from_dict(data: dict) -> tuple[list[list], list, list]: _data, index = _from_nested_column_dict(data) else: try: - _data = [list(x) for x in zip(*data.values())] + _data = [list(x) for x in zip(*data.values(), strict=False)] except TypeError as err: raise ValueError( "All values in the dict must be iterable (e.g. a list)." diff --git a/src/magicgui/widgets/bases/_button_widget.py b/src/magicgui/widgets/bases/_button_widget.py index 7978fac41..6ca5a9059 100644 --- a/src/magicgui/widgets/bases/_button_widget.py +++ b/src/magicgui/widgets/bases/_button_widget.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING from psygnal import Signal, SignalInstance diff --git a/src/magicgui/widgets/bases/_categorical_widget.py b/src/magicgui/widgets/bases/_categorical_widget.py index c5263e106..07e4319e2 100644 --- a/src/magicgui/widgets/bases/_categorical_widget.py +++ b/src/magicgui/widgets/bases/_categorical_widget.py @@ -1,7 +1,8 @@ from __future__ import annotations +from collections.abc import Callable from enum import Enum, EnumMeta -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, cast from magicgui.types import ChoicesType, Undefined, _Undefined diff --git a/src/magicgui/widgets/bases/_container_widget.py b/src/magicgui/widgets/bases/_container_widget.py index 9a2ebc31f..0e7421627 100644 --- a/src/magicgui/widgets/bases/_container_widget.py +++ b/src/magicgui/widgets/bases/_container_widget.py @@ -1,12 +1,11 @@ from __future__ import annotations import contextlib -from collections.abc import Iterable, Mapping, MutableSequence, Sequence +from collections.abc import Callable, Iterable, Mapping, MutableSequence, Sequence from itertools import chain from typing import ( TYPE_CHECKING, Any, - Callable, Generic, NoReturn, TypeVar, diff --git a/src/magicgui/widgets/bases/_ranged_widget.py b/src/magicgui/widgets/bases/_ranged_widget.py index 51558bd32..6c3685988 100644 --- a/src/magicgui/widgets/bases/_ranged_widget.py +++ b/src/magicgui/widgets/bases/_ranged_widget.py @@ -2,9 +2,9 @@ import builtins from abc import ABC, abstractmethod -from collections.abc import Iterable +from collections.abc import Callable, Iterable from math import ceil, log10 -from typing import TYPE_CHECKING, Callable, TypeVar, Union, cast +from typing import TYPE_CHECKING, TypeVar, Union, cast from magicgui.types import Undefined, _Undefined diff --git a/src/magicgui/widgets/bases/_slider_widget.py b/src/magicgui/widgets/bases/_slider_widget.py index 4118be688..cf152d9d6 100644 --- a/src/magicgui/widgets/bases/_slider_widget.py +++ b/src/magicgui/widgets/bases/_slider_widget.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING from magicgui.types import Undefined, _Undefined diff --git a/src/magicgui/widgets/bases/_toolbar.py b/src/magicgui/widgets/bases/_toolbar.py index 5dfa7e62a..c79641e44 100644 --- a/src/magicgui/widgets/bases/_toolbar.py +++ b/src/magicgui/widgets/bases/_toolbar.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, TypeVar, Union from ._widget import Widget diff --git a/src/magicgui/widgets/bases/_value_widget.py b/src/magicgui/widgets/bases/_value_widget.py index 31fa91bf3..96c0fc271 100644 --- a/src/magicgui/widgets/bases/_value_widget.py +++ b/src/magicgui/widgets/bases/_value_widget.py @@ -1,10 +1,19 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, Union, cast +from collections.abc import Callable +from typing import ( + TYPE_CHECKING, + Any, + Generic, + TypeVar, + Union, + cast, + get_args, + get_origin, +) from psygnal import Signal -from typing_extensions import get_args, get_origin from magicgui.types import Undefined, _Undefined diff --git a/src/magicgui/widgets/protocols.py b/src/magicgui/widgets/protocols.py index f052c2e2c..5807b0d34 100644 --- a/src/magicgui/widgets/protocols.py +++ b/src/magicgui/widgets/protocols.py @@ -11,10 +11,10 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Callable from typing import ( TYPE_CHECKING, Any, - Callable, NoReturn, Protocol, runtime_checkable, diff --git a/tests/test_backends.py b/tests/test_backends.py index 849698ec7..0a3cd62bb 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -13,9 +13,7 @@ def test_event(): """Test that the event loop makes a Qt app.""" if QtW.QApplication.instance(): - if API_NAME == "PySide2": - __import__("shiboken2").delete(QtW.QApplication.instance()) - elif API_NAME == "PySide6": + if API_NAME == "PySide6": __import__("shiboken6").delete(QtW.QApplication.instance()) else: raise AssertionError(f"known API name: {API_NAME}") diff --git a/tests/test_gui_class.py b/tests/test_gui_class.py index 3f457dd49..fb5e1310a 100644 --- a/tests/test_gui_class.py +++ b/tests/test_gui_class.py @@ -1,5 +1,4 @@ import contextlib -import sys from dataclasses import asdict, dataclass from typing import TYPE_CHECKING, ClassVar from unittest.mock import Mock @@ -131,9 +130,6 @@ class Foo: assert isinstance(foo.gui, Container) -@pytest.mark.skipif( - sys.version_info < (3, 11), reason="weakref_slot are python3.11 or higher" -) def test_slots_guiclass() -> None: """Test that the guiclass decorator works as expected.""" diff --git a/tests/test_table.py b/tests/test_table.py index 5d2c2d19d..42a231844 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -204,7 +204,7 @@ def test_dataview_getitem(index): assert np.allclose(table.data[index], data[index]) -@pytest.mark.parametrize("index, value", tuple(zip(INDICES, VALUES))) +@pytest.mark.parametrize("index, value", tuple(zip(INDICES, VALUES, strict=False))) def test_dataview_setitem(index, value): """Test that table.data can be indexed like a numpy array.""" np = pytest.importorskip("numpy") diff --git a/tests/test_types.py b/tests/test_types.py index 8b2ae8ee9..14ff41cc0 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,11 +1,10 @@ from collections.abc import Sequence from enum import Enum from pathlib import Path -from typing import Annotated, Optional, Union +from typing import Annotated, Optional, Union, get_args from unittest.mock import Mock import pytest -from typing_extensions import get_args from magicgui import magicgui, register_type, type_map, type_registered, types, widgets from magicgui.type_map import TypeMap diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 2a4ad2ac0..d6f428ae2 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1077,9 +1077,7 @@ def func(range_value: tuple[float, float] = (0.2, 0.8)): def test_literal(): - from typing import Literal - - from typing_extensions import get_args + from typing import Literal, get_args Lit = Literal[None, "a", 1, True, b"bytes"]