Skip to content

Commit c559e7e

Browse files
test: ensure everything works with minimal declared dependencies, test all examples (#702)
* feat: enhance example scripts and add dependency checks - Added dependency checks for 'pint', 'numpy', and 'scipy' in example scripts. - Updated image example to use pathlib for file paths. - Introduced a new test file to ensure example scripts run without errors. - Refactored widget tests to conditionally include numpy-related tests. - Improved GitHub Actions workflow to test on multiple OS environments. * require uv managed * remove none group * bump gallery * mkdocs-gallery hack * style(pre-commit.ci): auto fixes [...] * lint --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6179a0d commit c559e7e

File tree

11 files changed

+172
-61
lines changed

11 files changed

+172
-61
lines changed

.github/workflows/test_and_deploy.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ jobs:
5555
- python-version: "3.13"
5656
os: windows-latest
5757
add-group: pyqt6
58+
59+
5860
steps:
5961
- uses: actions/checkout@v4
6062
with:
@@ -99,6 +101,39 @@ jobs:
99101
path: ./.coverage*
100102
include-hidden-files: true
101103

104+
test-min-deps:
105+
name: min-deps ${{ matrix.os }} (${{ matrix.python-version }})
106+
runs-on: ${{ matrix.os }}
107+
env:
108+
UV_MANAGED_PYTHON: 1
109+
strategy:
110+
fail-fast: false
111+
matrix:
112+
python-version: ["3.9", "3.13"]
113+
os: [macos-latest]
114+
115+
steps:
116+
- uses: actions/checkout@v4
117+
with:
118+
fetch-depth: 0
119+
120+
- name: 🐍 Set up Python ${{ matrix.python-version }}
121+
uses: astral-sh/setup-uv@v6
122+
with:
123+
python-version: ${{ matrix.python-version }}
124+
enable-cache: true
125+
cache-dependency-glob: "**/pyproject.toml"
126+
127+
- name: 🧪 Run Tests
128+
run: uv run --no-dev --group pyqt6 coverage run -p -m pytest -v
129+
130+
- name: Upload coverage
131+
uses: actions/upload-artifact@v4
132+
with:
133+
name: covreport-mindeps-${{ matrix.os }}-py${{ matrix.python-version }}
134+
path: ./.coverage*
135+
include-hidden-files: true
136+
102137
upload_coverage:
103138
if: always()
104139
needs: [test]
@@ -158,4 +193,4 @@ jobs:
158193
- uses: softprops/action-gh-release@v2
159194
with:
160195
generate_release_notes: true
161-
files: './dist/*'
196+
files: "./dist/*"

docs/examples/applications/pint_quantity.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@
77
https://pint.readthedocs.io/en/stable/
88
"""
99

10-
from pint import Quantity
10+
try:
11+
from pint import Quantity
12+
except ImportError:
13+
msg = (
14+
"This example requires the pint package. "
15+
"To use magicgui with pint please `pip install pint`, "
16+
"or use the pint extra: `pip install magicgui[pint]`"
17+
)
18+
raise ImportError(msg) from None
1119

1220
from magicgui import magicgui
1321

docs/examples/demo_widgets/image.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
(This requires pillow, or that magicgui was installed as ``magicgui[image]``)
66
"""
77

8+
from pathlib import Path
9+
810
from magicgui.widgets import Image
911

10-
image = Image(value="../../images/_test.jpg")
12+
try:
13+
test_jpg = Path(__file__).parent.parent.parent / "images" / "_test.jpg"
14+
except NameError: # hack to support mkdocs-gallery build, which doesn't define __file__
15+
test_jpg = "../../images/_test.jpg"
16+
17+
image = Image(value=test_jpg)
1118
image.scale_widget_to_image_size()
1219
image.show(run=True)

docs/examples/demo_widgets/table.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
Demonstrating a few ways to input tables.
44
"""
55

6-
import numpy as np
6+
try:
7+
import numpy as np
8+
except ImportError:
9+
raise ImportError("This example requires the numpy package. ")
710

811
from magicgui.widgets import Table
912

docs/examples/matplotlib/waveform.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
import matplotlib.pyplot as plt
1212
import numpy as np
1313
from matplotlib.backends.backend_qt5agg import FigureCanvas
14-
from scipy import signal
14+
15+
try:
16+
from scipy import signal
17+
except ImportError:
18+
raise ImportError("This example requires the scipy package. ")
1519

1620
from magicgui import magicgui, register_type, widgets
1721

pyproject.toml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ dependencies = [
4747
# extras
4848
# https://peps.python.org/pep-0621/#dependencies-optional-dependencies
4949
[project.optional-dependencies]
50-
pyqt5 = [
51-
"PyQt5>=5.15.8",
52-
"pyqt5-qt5<=5.15.2; sys_platform == 'win32'"
53-
]
50+
pyqt5 = ["PyQt5>=5.15.8", "pyqt5-qt5<=5.15.2; sys_platform == 'win32'"]
5451
pyqt6 = ["pyqt6>=6.4.0"]
5552
pyside2 = ["pyside2>=5.15"]
5653
pyside6 = ["pyside6>=6.4.0"]
@@ -71,14 +68,13 @@ third-party-support = [
7168
"pydantic>=1.10.18",
7269
"toolz>=1.0.0",
7370
]
71+
test-min = ["pytest>=8.4.0", "pytest-cov >=6.1", "pytest-mypy-plugins>=3.1"]
72+
test-qt = [{ include-group = "test-min" }, "pytest-qt >=4.3.0"]
7473
test = [
7574
"magicgui[tqdm,jupyter,image,quantity]",
75+
{ include-group = "test-min" },
7676
{ include-group = "third-party-support" },
77-
"pytest>=8.4.0",
78-
"pytest-cov >=6.1",
79-
"pytest-mypy-plugins>=3.1",
8077
]
81-
test-qt = [{ include-group = "test" }, "pytest-qt >=4.3.0"]
8278
pyqt5 = ["magicgui[pyqt5]", { include-group = "test-qt" }]
8379
pyqt6 = ["magicgui[pyqt6]", { include-group = "test-qt" }]
8480
pyside2 = ["magicgui[pyside2]", { include-group = "test-qt" }]
@@ -99,10 +95,10 @@ docs = [
9995
"mkdocstrings ==0.26.1",
10096
"mkdocstrings-python ==1.11.1",
10197
"griffe ==1.2.0",
102-
"mkdocs-gen-files ==0.5.0",
103-
"mkdocs-literate-nav ==0.6.1",
98+
"mkdocs-gen-files >=0.5.0",
99+
"mkdocs-literate-nav >=0.6.1",
104100
"mkdocs-spellcheck[all] >=1.1.1",
105-
"mkdocs-gallery ==0.10.3",
101+
"mkdocs-gallery >=0.10.4",
106102
"qtgallery ==0.0.2",
107103
# extras for all the widgets
108104
"napari ==0.5.3",
@@ -179,6 +175,8 @@ filterwarnings = [
179175
"ignore:Jupyter is migrating:DeprecationWarning",
180176
"ignore:The `ipykernel.comm.Comm` class has been deprecated",
181177
"ignore:.*read_binary is deprecated:",
178+
"ignore:Pickle, copy, and deepcopy support:DeprecationWarning",
179+
"ignore:'count' is passed as positional argument::vispy",
182180
]
183181

184182
# https://mypy.readthedocs.io/en/stable/config_file.html

src/magicgui/type_map/_type_map.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,13 @@ def match_return_type(self, type_: Any) -> WidgetTuple | None:
166166
if type_ is widgets.Table:
167167
return widgets.Table, {}
168168

169-
table_types = [
170-
resolve_single_type(x) for x in ("pandas.DataFrame", "numpy.ndarray")
171-
]
169+
table_types = []
170+
for type_name in ("pandas.DataFrame", "numpy.ndarray"):
171+
try:
172+
table_types.append(resolve_single_type(type_name))
173+
except ModuleNotFoundError:
174+
# if the type cannot be resolved, it is not available
175+
pass
172176

173177
if any(
174178
safe_issubclass(type_, tt)

tests/test_examples.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import runpy
2+
from pathlib import Path
3+
from unittest.mock import patch
4+
5+
import pytest
6+
from qtpy.QtWidgets import QApplication
7+
8+
EXAMPLES_DIR = Path(__file__).parent.parent / "docs" / "examples"
9+
EXAMPLES = sorted(EXAMPLES_DIR.rglob("*.py"))
10+
11+
12+
@pytest.mark.parametrize(
13+
"example",
14+
EXAMPLES,
15+
ids=lambda p: str(p.relative_to(EXAMPLES_DIR)),
16+
)
17+
def test_example(qapp: QApplication, example: Path) -> None:
18+
"""Test that each example script runs without errors."""
19+
assert example.is_file()
20+
with patch.object(QApplication, "exec", lambda x: QApplication.processEvents()):
21+
try:
22+
runpy.run_path(str(example), run_name="__main__")
23+
except (ModuleNotFoundError, ImportError) as e:
24+
if "This example requires" in str(e):
25+
# if the error message indicates a missing required dependency
26+
# that's fine
27+
pytest.xfail(str(e))
28+
if "pip install magicgui[" in str(e):
29+
# if the error message indicates a missing optional dependency
30+
# that's fine
31+
pytest.xfail(str(e))
32+
if example.parent.name in str(e):
33+
# if the example is explicitly in a folder named after the
34+
# dependency it requires, that's fine
35+
pytest.xfail(str(e))
36+
raise

tests/test_magicgui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ def some_func(x: int, y: str) -> str:
823823

824824

825825
def test_curry():
826-
import toolz as tz
826+
tz = pytest.importorskip("toolz")
827827

828828
@tz.curry
829829
def some_func2(x: int, y: str) -> str:

tests/test_return_widgets.py

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,16 @@
33
"""Tests return widget types"""
44

55
import pathlib
6+
from contextlib import suppress
67
from datetime import date, datetime, time
78
from inspect import signature
89

9-
import numpy as np
1010
import pytest
1111

1212
import magicgui
1313
from magicgui import widgets
1414

1515

16-
def _dataframe_equals(object1, object2):
17-
assert object1.equals(object2)
18-
19-
20-
def _ndarray_equals(object1: np.ndarray, object2: np.ndarray):
21-
assert np.array_equal(object1, object2)
22-
23-
2416
def _default_equals(object1, object2):
2517
assert object1 == object2
2618

@@ -33,18 +25,6 @@ def _generate_pandas_test_data():
3325

3426

3527
parameterizations = [
36-
# pandas dataframe
37-
(
38-
_generate_pandas_test_data(),
39-
widgets.Table,
40-
_dataframe_equals,
41-
),
42-
# numpy array
43-
(
44-
np.array([1, 1, 1, 2, 2, 2, 3, 3, 3]).reshape((3, 3)),
45-
widgets.Table,
46-
_ndarray_equals,
47-
),
4828
# NOTE: disabling for now... these types are too broad to choose a table
4929
# # dict
5030
# ({"a": [1], "b": [2], "c": [3]}, widgets.Table, _default_equals),
@@ -74,6 +54,33 @@ def _generate_pandas_test_data():
7454
(slice(6), widgets.LineEdit, _default_equals),
7555
]
7656

57+
with suppress(ImportError):
58+
import numpy as np
59+
60+
def _ndarray_equals(object1: np.ndarray, object2: np.ndarray):
61+
assert np.array_equal(object1, object2)
62+
63+
parameterizations.append(
64+
(
65+
np.array([1, 1, 1, 2, 2, 2, 3, 3, 3]).reshape((3, 3)),
66+
widgets.Table,
67+
_ndarray_equals,
68+
)
69+
)
70+
71+
with suppress(ImportError):
72+
73+
def _dataframe_equals(object1, object2):
74+
assert object1.equals(object2)
75+
76+
parameterizations.append(
77+
(
78+
_generate_pandas_test_data(),
79+
widgets.Table,
80+
_dataframe_equals,
81+
)
82+
)
83+
7784

7885
def generate_magicgui(data):
7986
def func():

0 commit comments

Comments
 (0)