diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..21280e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,32 @@ +name: release + +on: + push: + branches: + - npe1 + - main + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + workflow_dispatch: + +jobs: + release: + runs-on: ubuntu-latest + if: contains(github.ref, 'tags') + permissions: + id-token: write + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools setuptools_scm wheel build + - name: build + run: | + python -m build . + - name: Publish + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test_pr.yml b/.github/workflows/test_pr.yml new file mode 100644 index 0000000..eaebe32 --- /dev/null +++ b/.github/workflows/test_pr.yml @@ -0,0 +1,64 @@ +name: tests + +on: + push: + branches: + - npe1 + - main + tags: + - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 + pull_request: + branches: + - npe1 + - main + workflow_dispatch: + +jobs: + test: + name: ${{ matrix.platform }} py${{ matrix.python-version }} + runs-on: ${{ matrix.platform }} + strategy: + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.8", "3.9", "3.10"] + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # these libraries, along with pytest-xvfb (added in the `deps` in tox.ini), + # 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' + uses: actions/checkout@v3 + with: + path: gl-ci-helpers + repository: pyvista/gl-ci-helpers + fetch-depth: 1 + - name: Install Windows OpenGL + if: runner.os == 'Windows' + run: | + 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 + pip install setuptools pytest pyqt5 + pip install . + - name: Test + uses: aganders3/headless-gui@v1 + with: + run: python -m pytest -s -v --color=yes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9ae7c2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,101 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +.dmypy.json + +# 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 +pip-wheel-metadata/ + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# mypy +.mypy_cache/ + +# Pycharm files +.idea + +# Liclipse +.project +.pydevproject +.settings/ + +# OS stuff +.DS_store + +# Benchmarking results +.asv/ + +# VSCode +.vscode/ + +# emacs +*~ +\#*\# +auto-save-list +tramp +.\#* +*_flymake.* +.projectile +.dir-locals.el + +github_cache.sqlite diff --git a/dummy_test_plugin/__init__.py b/dummy_test_plugin/__init__.py new file mode 100644 index 0000000..fa8444b --- /dev/null +++ b/dummy_test_plugin/__init__.py @@ -0,0 +1,110 @@ +from typing import Dict, List +import numpy as np +from napari_plugin_engine import napari_hook_implementation +from magicgui import magic_factory + +#### READER #### +@napari_hook_implementation +def napari_get_reader(path: str): + if not path.endswith(".npy"): + return None + return reader_function + + +#### SAMPLE #### +@napari_hook_implementation +def napari_provide_sample_data(): + return { + 'random data': generate_random_data, + 'random image': 'https://picsum.photos/1024', + 'sample_key': { + 'display_name': 'Some Random Data (512 x 512)', + 'data': generate_random_data, + } + } + + +#### WRITERS #### +@napari_hook_implementation +def napari_get_writer(path: str, layer_types: List[str]): + if not path.endswith('.npy'): + return None + + return save_numpy + + +@napari_hook_implementation +def napari_write_image(path:str, data: np.ndarray, meta: Dict): + if not path.endswith('.npy'): + return None + + saved_path = save_numpy(path, [(data, meta, 'image')]) + return saved_path + + +### WIDGETS ### +@napari_hook_implementation +def napari_experimental_provide_function(): + return my_function + + +@napari_hook_implementation +def napari_experimental_provide_dock_widget(): + return threshold + + +### THEME ### +@napari_hook_implementation +def napari_experimental_provide_theme(): + return get_new_theme() + + +### HELPERS ### +def reader_function(path: str): + data = np.load(path) + return [(data, {}, 'image')] + + +def generate_random_data(shape=(512, 512)): + data = np.random.rand(*shape) + return [(data, {'name': 'random data'})] + + +def save_numpy(path, layer_tuples): + data, _, _ = layer_tuples[0] + np.save(path, data) + return path + + +def my_function(image : 'napari.types.ImageData') -> 'napari.types.LayerDataTuple': + result = -image + return (result, {'colormap':'turbo'}, 'image') + + +@magic_factory(auto_call=True, threshold={'max': 2 ** 16}) +def threshold( + data: 'napari.types.ImageData', threshold: int +) -> 'napari.types.LabelsData': + return (data > threshold).astype(int) + + +def get_new_theme(): + themes = { + "super_dark": { + "name": "super_dark", + "background": "rgb(12, 12, 12)", + "foreground": "rgb(65, 72, 81)", + "primary": "rgb(90, 98, 108)", + "secondary": "rgb(134, 142, 147)", + "highlight": "rgb(106, 115, 128)", + "text": "rgb(240, 241, 242)", + "icon": "rgb(209, 210, 212)", + "warning": "rgb(153, 18, 31)", + "current": "rgb(0, 122, 204)", + "syntax_style": "native", + "console": "rgb(0, 0, 0)", + "canvas": "black", + } + } + return themes + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b1af833 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,22 @@ +[metadata] +name = dummy-test-plugin +version = 0.0.1 +author = napari team +description = A simple plugin for testing purposes +long_description = file: README.md +long_description_content_type = text/markdown +license = BSD-3-Clause +license_files = LICENSE + +[options] +packages = find: +install_requires = + magicgui + napari + napari-plugin-engine>=0.1.4 + numpy + qtpy + +[options.entry_points] +napari.plugin = + dummy-test-plugin = dummy_test_plugin diff --git a/tests/test_hook_impls.py b/tests/test_hook_impls.py new file mode 100644 index 0000000..b11713f --- /dev/null +++ b/tests/test_hook_impls.py @@ -0,0 +1,53 @@ +import numpy as np + +from dummy_test_plugin import ( + napari_get_reader, + napari_get_writer, + my_function, + threshold +) + + +def test_reader(tmp_path): + np_path = tmp_path / "test_file.npy" + data = np.ones(shape=(10, 10)) + np.save(np_path, data) + + reader = napari_get_reader(str(np_path)) + assert callable(reader) + read_tuples = reader(np_path) + assert len(read_tuples) == 1 + read_tuple = read_tuples[0] + np.testing.assert_allclose(data, read_tuple[0]) + + fake_path = tmp_path / "file.fake" + reader = napari_get_reader(str(fake_path)) + assert reader is None + +def test_get_writer(tmp_path): + np_path = tmp_path / "test_file.npy" + data = np.ones(shape=(10, 10)) + layer_tuple = (data, {}, 'image') + writer = napari_get_writer(str(np_path), ['image']) + assert callable(writer) + saved_path = writer(np_path, [layer_tuple]) + assert saved_path == np_path + loaded_data = np.load(saved_path) + np.testing.assert_allclose(data, loaded_data) + + fake_path = tmp_path / "file.fake" + writer = napari_get_writer(str(fake_path), ['image']) + assert writer is None + +def test_my_function(): + im_data = np.ones(shape=(10, 10), dtype=np.uint8) + func_result = my_function(im_data) + assert len(func_result) == 3 + data = func_result[0] + assert np.all(data == 255) + +def test_threshold(): + widget = threshold() + im_data = np.ones(shape=(10, 10), dtype=np.uint8) + thresholded_im = widget(im_data, 0) + assert np.all(thresholded_im)