diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..b451f5e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +# Dependabot configuration +# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuring-dependabot-version-updates#enabling-github-dependabot-version-updates +# https://til.simonwillison.net/github/dependabot-python-setup + +version: 2 +updates: +- package-ecosystem: pip + directory: "/" + schedule: + interval: monthly + groups: + python-packages: + patterns: + - "*" diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml new file mode 100644 index 0000000..878496a --- /dev/null +++ b/.github/workflows/test_and_deploy.yml @@ -0,0 +1,91 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: tests + +on: + push: + branches: + - main + - npe2 + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + pull_request: + branches: + - main + - npe2 + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.platform }} py${{ matrix.python-version }} + runs-on: ${{ matrix.platform }} + timeout-minutes: 30 + strategy: + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + # these libraries enable testing on Qt on linux + - uses: tlambert03/setup-qt-libs@v1 + + # strategy borrowed from vispy for installing opengl libs on windows + - name: Install Windows OpenGL + if: runner.os == 'Windows' + run: | + git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git + powershell gl-ci-helpers/appveyor/install_opengl.ps1 + + # note: if you need dependencies from conda, considering using + # setup-miniconda: https://github.com/conda-incubator/setup-miniconda + # and + # tox-conda: https://github.com/tox-dev/tox-conda + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install setuptools tox tox-gh-actions + + # this runs the platform-specific tests declared in tox.ini + - name: Test with tox + uses: aganders3/headless-gui@v2 + with: + run: python -m tox + env: + PLATFORM: ${{ matrix.platform }} + + - name: Coverage + uses: codecov/codecov-action@v3 + + deploy: + # this will run when you have tagged a commit, starting with "v*" + # and requires that you have put your twine API key in your + # github secrets (see readme for details) + needs: [test] + runs-on: ubuntu-latest + if: contains(github.ref, 'tags') + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools setuptools_scm wheel twine build + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }} + run: | + git tag + python -m build . + twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73d56d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,84 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +.napari_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask instance folder +instance/ + +# Sphinx documentation +docs/_build/ + +# MkDocs documentation +/site/ + +# PyBuilder +target/ + +# Pycharm and VSCode +.idea/ +venv/ +.vscode/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# OS +.DS_Store + +# written by setuptools_scm +**/_version.py diff --git a/.napari-hub/DESCRIPTION.md b/.napari-hub/DESCRIPTION.md new file mode 100644 index 0000000..7ecdd97 --- /dev/null +++ b/.napari-hub/DESCRIPTION.md @@ -0,0 +1,9 @@ + + +The developer has not yet provided a napari-hub specific description. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..43c3bb3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-docstring-first + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: ^\.napari-hub/.* + - id: check-yaml # checks for correct yaml syntax for github actions ex. + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.4 + hooks: + - id: ruff + - repo: https://github.com/psf/black + rev: 24.4.2 + hooks: + - id: black + - repo: https://github.com/tlambert03/napari-plugin-checks + rev: v0.3.0 + hooks: + - id: napari-plugin-checks + # https://mypy.readthedocs.io/en/stable/ + # you may wish to add this as well! + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: v1.9.0 + # hooks: + # - id: mypy diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8c4e427 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ + +Copyright (c) 2024, Joel Luethi, Adrian Tschan +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f3155af --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include LICENSE +include README.md + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cafbca --- /dev/null +++ b/README.md @@ -0,0 +1,67 @@ +# napari-feature-visualization + +[![License BSD-3](https://img.shields.io/pypi/l/napari-feature-visualization.svg?color=green)](https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/raw/main/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/napari-feature-visualization.svg?color=green)](https://pypi.org/project/napari-feature-visualization) +[![Python Version](https://img.shields.io/pypi/pyversions/napari-feature-visualization.svg?color=green)](https://python.org) +[![tests](https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/workflows/tests/badge.svg)](https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/actions) +[![codecov](https://codecov.io/gh/fractal-napari-plugins-collection/napari-feature-visualization/branch/main/graph/badge.svg)](https://codecov.io/gh/fractal-napari-plugins-collection/napari-feature-visualization) +[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-feature-visualization)](https://napari-hub.org/plugins/napari-feature-visualization) + +Visualizing feature measurements on label images in napari + +---------------------------------- + +This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template. + + + +## Installation + +You can install `napari-feature-visualization` via [pip]: + + pip install napari-feature-visualization + + + +To install latest development version : + + pip install git+https://github.com/fractal-napari-plugins-collection/napari-feature-visualization.git + + +## Contributing + +Contributions are very welcome. Tests can be run with [tox], please ensure +the coverage at least stays the same before you submit a pull request. + +## License + +Distributed under the terms of the [BSD-3] license, +"napari-feature-visualization" is free and open source software + +## Issues + +If you encounter any problems, please [file an issue] along with a detailed description. + +[napari]: https://github.com/napari/napari +[Cookiecutter]: https://github.com/audreyr/cookiecutter +[@napari]: https://github.com/napari +[MIT]: http://opensource.org/licenses/MIT +[BSD-3]: http://opensource.org/licenses/BSD-3-Clause +[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt +[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt +[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0 +[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt +[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin + +[file an issue]: https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/issues + +[napari]: https://github.com/napari/napari +[tox]: https://tox.readthedocs.io/en/latest/ +[pip]: https://pypi.org/project/pip/ +[PyPI]: https://pypi.org/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fa16ad2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,119 @@ +[project] +name = "napari-feature-visualization" +dynamic = ["version"] +description = "Visualizing feature measurements on label images in napari" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "Joel Luethi, Adrian Tschan"}, + {email = "joel.luethi@uzh.ch"}, +] +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Framework :: napari", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "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", + "Topic :: Scientific/Engineering :: Image Processing", +] +requires-python = ">=3.9" +dependencies = [ + "numpy", + "magicgui", + "qtpy", + "scikit-image", +] + +[project.optional-dependencies] +testing = [ + "tox", + "pytest", # https://docs.pytest.org/en/latest/contents.html + "pytest-cov", # https://pytest-cov.readthedocs.io/en/latest/ + "pytest-qt", # https://pytest-qt.readthedocs.io/en/latest/ + "napari", + "pyqt5", +] + +[project.entry-points."napari.manifest"] +napari-feature-visualization = "napari_feature_visualization:napari.yaml" + +[project.urls] +"Bug Tracker" = "https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/issues" +"Documentation" = "https://github.com/fractal-napari-plugins-collection/napari-feature-visualization#README.md" +"Source Code" = "https://github.com/fractal-napari-plugins-collection/napari-feature-visualization" +"User Support" = "https://github.com/fractal-napari-plugins-collection/napari-feature-visualization/issues" + +[build-system] +requires = ["setuptools>=42.0.0", "wheel", "setuptools_scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"*" = ["*.yaml"] + + +[tool.setuptools_scm] +write_to = "src/napari_feature_visualization/_version.py" + + +[tool.black] +line-length = 79 +target-version = ['py38', 'py39', 'py310'] + +[tool.ruff] +line-length = 79 +lint.select = [ + "E", "F", "W", #flake8 + "UP", # pyupgrade + "I", # isort + "BLE", # flake8-blind-exception + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "ISC", # flake8-implicit-str-concat + "G", # flake8-logging-format + "PIE", # flake8-pie + "SIM", # flake8-simplify +] +lint.ignore = [ + "E501", # line too long. let black handle this + "UP006", "UP007", # type annotation. As using magicgui require runtime type annotation then we disable this. + "SIM117", # flake8-simplify - some of merged with statements are not looking great with black, reanble after drop python 3.9 +] + +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".mypy_cache", + ".pants.d", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "*vendored*", + "*_vendor*", +] + +target-version = "py38" +fix = true diff --git a/src/napari_feature_visualization/__init__.py b/src/napari_feature_visualization/__init__.py new file mode 100644 index 0000000..8893440 --- /dev/null +++ b/src/napari_feature_visualization/__init__.py @@ -0,0 +1,19 @@ +try: + from ._version import version as __version__ +except ImportError: + __version__ = "unknown" +from ._sample_data import make_sample_data +from ._widget import ( + ExampleQWidget, + ImageThreshold, + threshold_autogenerate_widget, + threshold_magic_widget, +) + +__all__ = ( + "make_sample_data", + "ExampleQWidget", + "ImageThreshold", + "threshold_autogenerate_widget", + "threshold_magic_widget", +) diff --git a/src/napari_feature_visualization/_sample_data.py b/src/napari_feature_visualization/_sample_data.py new file mode 100644 index 0000000..203451f --- /dev/null +++ b/src/napari_feature_visualization/_sample_data.py @@ -0,0 +1,22 @@ +""" +This module is an example of a barebones sample data provider for napari. + +It implements the "sample data" specification. +see: https://napari.org/stable/plugins/guides.html?#sample-data + +Replace code below according to your needs. +""" + +from __future__ import annotations + +import numpy + + +def make_sample_data(): + """Generates an image""" + # Return list of tuples + # [(data1, add_image_kwargs1), (data2, add_image_kwargs2)] + # Check the documentation for more information about the + # add_image_kwargs + # https://napari.org/stable/api/napari.Viewer.html#napari.Viewer.add_image + return [(numpy.random.rand(512, 512), {})] diff --git a/src/napari_feature_visualization/_tests/__init__.py b/src/napari_feature_visualization/_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/napari_feature_visualization/_tests/test_sample_data.py b/src/napari_feature_visualization/_tests/test_sample_data.py new file mode 100644 index 0000000..14a5816 --- /dev/null +++ b/src/napari_feature_visualization/_tests/test_sample_data.py @@ -0,0 +1,7 @@ +# from napari_feature_visualization import make_sample_data + +# add your tests here... + + +def test_something(): + pass diff --git a/src/napari_feature_visualization/_tests/test_widget.py b/src/napari_feature_visualization/_tests/test_widget.py new file mode 100644 index 0000000..3eef34d --- /dev/null +++ b/src/napari_feature_visualization/_tests/test_widget.py @@ -0,0 +1,66 @@ +import numpy as np + +from napari_feature_visualization._widget import ( + ExampleQWidget, + ImageThreshold, + threshold_autogenerate_widget, + threshold_magic_widget, +) + + +def test_threshold_autogenerate_widget(): + # because our "widget" is a pure function, we can call it and + # test it independently of napari + im_data = np.random.random((100, 100)) + thresholded = threshold_autogenerate_widget(im_data, 0.5) + assert thresholded.shape == im_data.shape + # etc. + + +# make_napari_viewer is a pytest fixture that returns a napari viewer object +# you don't need to import it, as long as napari is installed +# in your testing environment +def test_threshold_magic_widget(make_napari_viewer): + viewer = make_napari_viewer() + layer = viewer.add_image(np.random.random((100, 100))) + + # our widget will be a MagicFactory or FunctionGui instance + my_widget = threshold_magic_widget() + + # if we "call" this object, it'll execute our function + thresholded = my_widget(viewer.layers[0], 0.5) + assert thresholded.shape == layer.data.shape + # etc. + + +def test_image_threshold_widget(make_napari_viewer): + viewer = make_napari_viewer() + layer = viewer.add_image(np.random.random((100, 100))) + my_widget = ImageThreshold(viewer) + + # because we saved our widgets as attributes of the container + # we can set their values without having to "interact" with the viewer + my_widget._image_layer_combo.value = layer + my_widget._threshold_slider.value = 0.5 + + # this allows us to run our functions directly and ensure + # correct results + my_widget._threshold_im() + assert len(viewer.layers) == 2 + + +# capsys is a pytest fixture that captures stdout and stderr output streams +def test_example_q_widget(make_napari_viewer, capsys): + # make viewer and add an image layer using our fixture + viewer = make_napari_viewer() + viewer.add_image(np.random.random((100, 100))) + + # create our widget, passing in the viewer + my_widget = ExampleQWidget(viewer) + + # call our widget method + my_widget._on_click() + + # read captured output and check that it's as we expected + captured = capsys.readouterr() + assert captured.out == "napari has 1 layers\n" diff --git a/src/napari_feature_visualization/_widget.py b/src/napari_feature_visualization/_widget.py new file mode 100644 index 0000000..b4f9058 --- /dev/null +++ b/src/napari_feature_visualization/_widget.py @@ -0,0 +1,129 @@ +""" +This module contains four napari widgets declared in +different ways: + +- a pure Python function flagged with `autogenerate: true` + in the plugin manifest. Type annotations are used by + magicgui to generate widgets for each parameter. Best + suited for simple processing tasks - usually taking + in and/or returning a layer. +- a `magic_factory` decorated function. The `magic_factory` + decorator allows us to customize aspects of the resulting + GUI, including the widgets associated with each parameter. + Best used when you have a very simple processing task, + but want some control over the autogenerated widgets. If you + find yourself needing to define lots of nested functions to achieve + your functionality, maybe look at the `Container` widget! +- a `magicgui.widgets.Container` subclass. This provides lots + of flexibility and customization options while still supporting + `magicgui` widgets and convenience methods for creating widgets + from type annotations. If you want to customize your widgets and + connect callbacks, this is the best widget option for you. +- a `QWidget` subclass. This provides maximal flexibility but requires + full specification of widget layouts, callbacks, events, etc. + +References: +- Widget specification: https://napari.org/stable/plugins/guides.html?#widgets +- magicgui docs: https://pyapp-kit.github.io/magicgui/ + +Replace code below according to your needs. +""" + +from typing import TYPE_CHECKING + +from magicgui import magic_factory +from magicgui.widgets import CheckBox, Container, create_widget +from qtpy.QtWidgets import QHBoxLayout, QPushButton, QWidget +from skimage.util import img_as_float + +if TYPE_CHECKING: + import napari + + +# Uses the `autogenerate: true` flag in the plugin manifest +# to indicate it should be wrapped as a magicgui to autogenerate +# a widget. +def threshold_autogenerate_widget( + img: "napari.types.ImageData", + threshold: "float", +) -> "napari.types.LabelsData": + return img_as_float(img) > threshold + + +# the magic_factory decorator lets us customize aspects of our widget +# we specify a widget type for the threshold parameter +# and use auto_call=True so the function is called whenever +# the value of a parameter changes +@magic_factory( + threshold={"widget_type": "FloatSlider", "max": 1}, auto_call=True +) +def threshold_magic_widget( + img_layer: "napari.layers.Image", threshold: "float" +) -> "napari.types.LabelsData": + return img_as_float(img_layer.data) > threshold + + +# if we want even more control over our widget, we can use +# magicgui `Container` +class ImageThreshold(Container): + def __init__(self, viewer: "napari.viewer.Viewer"): + super().__init__() + self._viewer = viewer + # use create_widget to generate widgets from type annotations + self._image_layer_combo = create_widget( + label="Image", annotation="napari.layers.Image" + ) + self._threshold_slider = create_widget( + label="Threshold", annotation=float, widget_type="FloatSlider" + ) + self._threshold_slider.min = 0 + self._threshold_slider.max = 1 + # use magicgui widgets directly + self._invert_checkbox = CheckBox(text="Keep pixels below threshold") + + # connect your own callbacks + self._threshold_slider.changed.connect(self._threshold_im) + self._invert_checkbox.changed.connect(self._threshold_im) + + # append into/extend the container with your widgets + self.extend( + [ + self._image_layer_combo, + self._threshold_slider, + self._invert_checkbox, + ] + ) + + def _threshold_im(self): + image_layer = self._image_layer_combo.value + if image_layer is None: + return + + image = img_as_float(image_layer.data) + name = image_layer.name + "_thresholded" + threshold = self._threshold_slider.value + if self._invert_checkbox.value: + thresholded = image < threshold + else: + thresholded = image > threshold + if name in self._viewer.layers: + self._viewer.layers[name].data = thresholded + else: + self._viewer.add_labels(thresholded, name=name) + + +class ExampleQWidget(QWidget): + # your QWidget.__init__ can optionally request the napari viewer instance + # use a type annotation of 'napari.viewer.Viewer' for any parameter + def __init__(self, viewer: "napari.viewer.Viewer"): + super().__init__() + self.viewer = viewer + + btn = QPushButton("Click me!") + btn.clicked.connect(self._on_click) + + self.setLayout(QHBoxLayout()) + self.layout().addWidget(btn) + + def _on_click(self): + print("napari has", len(self.viewer.layers), "layers") diff --git a/src/napari_feature_visualization/napari.yaml b/src/napari_feature_visualization/napari.yaml new file mode 100644 index 0000000..750c0e7 --- /dev/null +++ b/src/napari_feature_visualization/napari.yaml @@ -0,0 +1,37 @@ +name: napari-feature-visualization +display_name: Napari Feature Visualization +# use 'hidden' to remove plugin from napari hub search results +visibility: public +# see https://napari.org/stable/plugins/manifest.html for valid categories +categories: ["Annotation", "Segmentation", "Acquisition"] +contributions: + commands: + - id: napari-feature-visualization.make_sample_data + python_name: napari_feature_visualization._sample_data:make_sample_data + title: Load sample data from Napari Feature Visualization + - id: napari-feature-visualization.make_container_widget + python_name: napari_feature_visualization:ImageThreshold + title: Make threshold Container widget + - id: napari-feature-visualization.make_magic_widget + python_name: napari_feature_visualization:threshold_magic_widget + title: Make threshold magic widget + - id: napari-feature-visualization.make_function_widget + python_name: napari_feature_visualization:threshold_autogenerate_widget + title: Make threshold function widget + - id: napari-feature-visualization.make_qwidget + python_name: napari_feature_visualization:ExampleQWidget + title: Make example QWidget + sample_data: + - command: napari-feature-visualization.make_sample_data + display_name: Napari Feature Visualization + key: unique_id.1 + widgets: + - command: napari-feature-visualization.make_container_widget + display_name: Container Threshold + - command: napari-feature-visualization.make_magic_widget + display_name: Magic Threshold + - command: napari-feature-visualization.make_function_widget + autogenerate: true + display_name: Autogenerate Threshold + - command: napari-feature-visualization.make_qwidget + display_name: Example QWidget diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..58c2841 --- /dev/null +++ b/tox.ini @@ -0,0 +1,33 @@ +# For more information about tox, see https://tox.readthedocs.io/en/latest/ +[tox] +envlist = py{39,310,311,312}-{linux,macos,windows} +isolated_build=true + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows + +[testenv] +platform = + macos: darwin + linux: linux + windows: win32 +passenv = + CI + GITHUB_ACTIONS + DISPLAY + XAUTHORITY + NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +extras = + testing +commands = pytest -v --color=yes --cov=napari_feature_visualization --cov-report=xml