diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5b721d9..39c1ab5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: sudo apt install graphviz --yes - name: Build Docs - uses: aganders3/headless-gui@v1 + uses: aganders3/headless-gui@v2 with: run: make html working-directory: ./docs diff --git a/.github/workflows/napari_hub_preview.yml b/.github/workflows/napari_hub_preview.yml deleted file mode 100644 index c204ac4..0000000 --- a/.github/workflows/napari_hub_preview.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: napari hub Preview Page # we use this name to find your preview page artifact, so don't change it! -# For more info on this action, see https://github.com/chanzuckerberg/napari-hub-preview-action/blob/main/action.yml - -on: - pull_request: - types: [ labeled ] - -jobs: - preview-page: - if: ${{ github.event.label.name == 'napari hub preview' }} - name: Preview Page Deploy - runs-on: ubuntu-latest - - steps: - - name: Checkout repo - uses: actions/checkout@v3 - - - name: napari hub Preview Page Builder - uses: chanzuckerberg/napari-hub-preview-action@v0.1 - with: - hub-ref: main diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 8665e1d..2a8b731 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -12,6 +12,10 @@ on: workflow_dispatch: merge_group: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: name: ${{ matrix.platform }} py${{ matrix.python-version }} @@ -20,7 +24,7 @@ jobs: fail-fast: false matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.9', '3.10', '3.11'] + python-version: ['3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v3 @@ -58,13 +62,15 @@ jobs: if: ${{ always() }} - name: Coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 # Don't run coverage on merge queue CI to avoid duplicating reports # to codecov. See https://github.com/matplotlib/napari-matplotlib/issues/155 if: github.event_name != 'merge_group' with: token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true + fail_ci_if_error: false + + deploy: # this will run when you have tagged a commit, starting with "v*" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8df635a..ebae2d3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/psf/black - rev: 23.12.1 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 hooks: - id: black @@ -17,14 +17,14 @@ repos: - id: napari-plugin-checks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.8.0 + rev: v1.10.1 hooks: - id: mypy additional_dependencies: [numpy, matplotlib] - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: 'v0.1.11' + rev: 'v0.5.1' hooks: - id: ruff diff --git a/docs/changelog.rst b/docs/changelog.rst index 255982a..697e483 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,18 @@ Changelog ========= + +2.0.2 +----- +Dependencies +~~~~~~~~~~~~ +napari-matplotlib now adheres to `SPEC 0 `_, and has: + +- Dropped support for Python 3.9 +- Added support for Python 3.12 +- Added a minimum required numpy verison of 1.23 +- Pinned the maximum napari version to ``< 0.5``. + Version 3.0 of ``napari-matplotlib`` will introduce support for ``napari`` version 0.5. + 2.0.1 ----- Bug fixes diff --git a/docs/conf.py b/docs/conf.py index 2517a59..f153383 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) -import qtgallery +from sphinx_gallery import scrapers # -- Project information ----------------------------------------------------- @@ -35,18 +35,58 @@ "sphinx.ext.intersphinx", ] + +def reset_napari(gallery_conf, fname): # type: ignore[no-untyped-def] + from napari.settings import get_settings + from qtpy.QtWidgets import QApplication + + settings = get_settings() + settings.appearance.theme = "dark" + + # Disabling `QApplication.exec_` means example scripts can call `exec_` + # (scripts work when run normally) without blocking example execution by + # sphinx-gallery. (from qtgallery) + QApplication.exec_ = lambda _: None + + +def napari_scraper(block, block_vars, gallery_conf): # type: ignore[no-untyped-def] + """Basic napari window scraper. + + Looks for any QtMainWindow instances and takes a screenshot of them. + + `app.processEvents()` allows Qt events to propagateo and prevents hanging. + """ + import napari + + imgpath_iter = block_vars["image_path_iterator"] + + if app := napari.qt.get_app(): + app.processEvents() + else: + return "" + + img_paths = [] + for win, img_path in zip( + reversed(napari._qt.qt_main_window._QtMainWindow._instances), + imgpath_iter, + strict=False, + ): + img_paths.append(img_path) + win._window.screenshot(img_path, canvas_only=False) + + napari.Viewer.close_all() + app.processEvents() + + return scrapers.figure_rst(img_paths, gallery_conf["src_dir"]) + + sphinx_gallery_conf = { "filename_pattern": ".", - "image_scrapers": (qtgallery.qtscraper,), - "reset_modules": (qtgallery.reset_qapp,), + "image_scrapers": (napari_scraper,), + "reset_modules": (reset_napari,), } +suppress_warnings = ["config.cache"] -qtgallery_conf = { - "xvfb_size": (640, 480), - "xvfb_color_depth": 24, - "xfvb_use_xauth": False, - "xfvb_extra_args": [], -} numpydoc_show_class_members = False automodapi_inheritance_diagram = True diff --git a/examples/histogram.py b/examples/histogram.py index ccda491..b9ceb37 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -2,6 +2,7 @@ Histograms ========== """ + import napari viewer = napari.Viewer() diff --git a/examples/scatter.py b/examples/scatter.py index cd81240..00e01ec 100644 --- a/examples/scatter.py +++ b/examples/scatter.py @@ -2,6 +2,7 @@ Scatter plots ============= """ + import napari viewer = napari.Viewer() diff --git a/examples/slice.py b/examples/slice.py index 3e43443..242a16c 100644 --- a/examples/slice.py +++ b/examples/slice.py @@ -2,6 +2,7 @@ 1D slices ========= """ + import napari viewer = napari.Viewer() diff --git a/pyproject.toml b/pyproject.toml index ba9f9e6..05f7df6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools", "wheel", "setuptools_scm"] +requires = ["setuptools", "setuptools_scm"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] @@ -12,9 +12,15 @@ filterwarnings = [ # Coming from vispy "ignore:distutils Version classes are deprecated:DeprecationWarning", "ignore:`np.bool8` is a deprecated alias for `np.bool_`:DeprecationWarning", + # Coming from pydantic via napari + "ignore:Pickle, copy, and deepcopy support will be removed from itertools in Python 3.14.:DeprecationWarning" ] qt_api = "pyqt6" -addopts = "--mpl --mpl-baseline-relative" +addopts = ["--mpl", "--mpl-baseline-relative", "--strict-config", "--strict-markers", "-ra"] +minversion = "7" +testpaths = ["src/napari_matplotlib/tests"] +log_cli_level = "INFO" +xfail_strict = true [tool.black] line-length = 79 @@ -24,8 +30,11 @@ profile = "black" line_length = 79 [tool.ruff] -target-version = "py39" -select = ["I", "UP", "F", "E", "W", "D"] +target-version = "py310" +fix = true + +[tool.ruff.lint] +select = ["B", "I", "UP", "F", "E", "W", "D"] ignore = [ "D100", # Missing docstring in public module "D104", # Missing docstring in public package @@ -35,24 +44,25 @@ ignore = [ "D401", # First line of docstring should be in imperative mood ] -fix = true -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "docs/*" = ["D"] "examples/*" = ["D"] "src/napari_matplotlib/tests/*" = ["D"] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "numpy" [tool.mypy] -python_version = "3.9" +python_version = "3.10" # Block below are checks that form part of mypy 'strict' mode strict = true disallow_subclassing_any = false # TODO: fix warn_return_any = false # TODO: fix ignore_missing_imports = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] + [[tool.mypy.overrides]] module = [ "napari_matplotlib/tests/*", diff --git a/setup.cfg b/setup.cfg index 41e4e34..073478a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -28,10 +28,10 @@ project_urls = packages = find: install_requires = matplotlib - napari - numpy + napari<0.5 + numpy>=1.23 tinycss2 -python_requires = >=3.9 +python_requires = >=3.10 include_package_data = True package_dir = =src @@ -47,11 +47,10 @@ napari.manifest = [options.extras_require] docs = - napari[all]==0.4.19rc3 + napari[all] numpydoc pydantic<2 pydata-sphinx-theme - qtgallery sphinx sphinx-automodapi sphinx-gallery diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index fb9e485..c455335 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from typing import Optional import matplotlib.style as mplstyle import napari @@ -38,7 +37,7 @@ class BaseNapariMPLWidget(QWidget): def __init__( self, napari_viewer: napari.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(parent=parent) self.viewer = napari_viewer @@ -173,7 +172,7 @@ class NapariMPLWidget(BaseNapariMPLWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(napari_viewer=napari_viewer, parent=parent) self._setup_callbacks() @@ -282,7 +281,7 @@ class SingleAxesWidget(NapariMPLWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(napari_viewer=napari_viewer, parent=parent) self.add_single_axes() diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 4076528..adbbae6 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, cast +from typing import Any, cast import napari import numpy as np @@ -44,7 +44,7 @@ class HistogramWidget(SingleAxesWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(napari_viewer, parent=parent) self._update_layers(None) @@ -60,7 +60,7 @@ def on_update_layers(self) -> None: def _update_contrast_lims(self) -> None: for lim, line in zip( - self.layers[0].contrast_limits, self._contrast_lines + self.layers[0].contrast_limits, self._contrast_lines, strict=False ): line.set_xdata(lim) @@ -121,7 +121,7 @@ class FeaturesHistogramWidget(SingleAxesWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(napari_viewer, parent=parent) @@ -137,12 +137,12 @@ def __init__( self._update_layers(None) @property - def x_axis_key(self) -> Optional[str]: + def x_axis_key(self) -> str | None: """Key to access x axis data from the FeaturesTable""" return self._x_axis_key @x_axis_key.setter - def x_axis_key(self, key: Optional[str]) -> None: + def x_axis_key(self, key: str | None) -> None: self._x_axis_key = key self._draw() @@ -166,7 +166,7 @@ def _get_valid_axis_keys(self) -> list[str]: else: return self.layers[0].features.keys() - def _get_data(self) -> tuple[Optional[npt.NDArray[Any]], str]: + def _get_data(self) -> tuple[npt.NDArray[Any] | None, str]: """Get the plot data. Returns diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 67d6896..98ebe92 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union +from typing import Any import napari import numpy.typing as npt @@ -100,7 +100,7 @@ class FeaturesScatterWidget(ScatterBaseWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): super().__init__(napari_viewer, parent=parent) @@ -118,7 +118,7 @@ def __init__( self._update_layers(None) @property - def x_axis_key(self) -> Union[str, None]: + def x_axis_key(self) -> str | None: """ Key for the x-axis data. """ @@ -133,7 +133,7 @@ def x_axis_key(self, key: str) -> None: self._draw() @property - def y_axis_key(self) -> Union[str, None]: + def y_axis_key(self) -> str | None: """ Key for the y-axis data. """ diff --git a/src/napari_matplotlib/slice.py b/src/napari_matplotlib/slice.py index 9459fa9..1924bf2 100644 --- a/src/napari_matplotlib/slice.py +++ b/src/napari_matplotlib/slice.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any import matplotlib.ticker as mticker import napari @@ -30,7 +30,7 @@ class SliceWidget(SingleAxesWidget): def __init__( self, napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None, + parent: QWidget | None = None, ): # Setup figure/axes super().__init__(napari_viewer, parent=parent) diff --git a/src/napari_matplotlib/tests/test_util.py b/src/napari_matplotlib/tests/test_util.py index a8792d4..e966cc2 100644 --- a/src/napari_matplotlib/tests/test_util.py +++ b/src/napari_matplotlib/tests/test_util.py @@ -26,7 +26,7 @@ def test_interval(): assert 10 not in interval with pytest.raises(ValueError, match="must be an integer"): - "string" in interval # type: ignore + assert "string" in interval # type: ignore[operator] with pytest.raises(ValueError, match="must be <= upper_bound"): Interval(5, 3) @@ -69,7 +69,10 @@ def test_fallback_if_missing_dimensions(mocker): test_css = " Flobble { background-color: rgb(0, 97, 163); } " mocker.patch("napari.qt.get_current_stylesheet").return_value = test_css with pytest.warns(RuntimeWarning, match="Unable to find DimensionToken"): - assert from_napari_css_get_size_of("Flobble", (1, 2)) == QSize(1, 2) + with pytest.warns(RuntimeWarning, match="Unable to find Flobble"): + assert from_napari_css_get_size_of("Flobble", (1, 2)) == QSize( + 1, 2 + ) def test_fallback_if_prelude_not_in_css(): diff --git a/src/napari_matplotlib/util.py b/src/napari_matplotlib/util.py index ed99425..8d4150c 100644 --- a/src/napari_matplotlib/util.py +++ b/src/napari_matplotlib/util.py @@ -1,4 +1,3 @@ -from typing import Optional, Union from warnings import warn import napari.qt @@ -12,7 +11,7 @@ class Interval: An integer interval. """ - def __init__(self, lower_bound: Optional[int], upper_bound: Optional[int]): + def __init__(self, lower_bound: int | None, upper_bound: int | None): """ Parameters ---------- @@ -48,7 +47,7 @@ def __contains__(self, val: int) -> bool: return True @property - def _helper_text(self) -> Optional[str]: + def _helper_text(self) -> str | None: """ Helper text for widgets. """ @@ -86,9 +85,7 @@ def _has_id(nodes: list[tinycss2.ast.Node], id_name: str) -> bool: ) -def _get_dimension( - nodes: list[tinycss2.ast.Node], id_name: str -) -> Union[int, None]: +def _get_dimension(nodes: list[tinycss2.ast.Node], id_name: str) -> int | None: """ Get the value of the DimensionToken for the IdentToken `id_name`. @@ -97,14 +94,18 @@ def _get_dimension( None if no IdentToken is found. """ cleaned_nodes = [node for node in nodes if node.type != "whitespace"] - for name, _, value, _ in zip(*(iter(cleaned_nodes),) * 4): + for name, _, value, _ in zip(*(iter(cleaned_nodes),) * 4, strict=False): if ( name.type == "ident" and value.type == "dimension" and name.value == id_name ): return value.int_value - warn(f"Unable to find DimensionToken for {id_name}", RuntimeWarning) + warn( + f"Unable to find DimensionToken for {id_name}", + RuntimeWarning, + stacklevel=1, + ) return None @@ -137,6 +138,7 @@ def from_napari_css_get_size_of( f"Unable to find {qt_element_name} or unable to find its size in " f"the current Napari stylesheet, falling back to {fallback}", RuntimeWarning, + stacklevel=1, ) return QSize(*fallback) diff --git a/tox.ini b/tox.ini index 4ec0c70..f4aed6a 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,12 @@ [tox] -envlist = py{39,310,311} +envlist = py{310,311,312} isolated_build = true [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 + 3.12: py312 [testenv] extras = testing