Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,26 +23,20 @@ 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:
- os: windows-latest
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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 0 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`

Expand Down
35 changes: 18 additions & 17 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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",
Expand Down Expand Up @@ -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"]
Expand All @@ -78,31 +75,27 @@ 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" },
{ include-group = "third-party-support" },
]
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",
Expand All @@ -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 }

Expand All @@ -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
Expand All @@ -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"]
Expand Down
3 changes: 2 additions & 1 deletion src/magicgui/_type_resolution.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/magicgui/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/magicgui/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/magicgui/backends/_ipynb/widgets.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions src/magicgui/schema/_guiclass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
9 changes: 5 additions & 4 deletions src/magicgui/schema/_ui_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@
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
from typing import (
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:
Expand All @@ -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")


Expand Down
5 changes: 2 additions & 3 deletions src/magicgui/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion src/magicgui/type_map/_magicgui.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/magicgui/type_map/_type_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down
Loading
Loading