Skip to content

Commit 7ff5ad6

Browse files
authored
Initial Implementation of ndev-workflows
This is the initial, large not quite MVP implementation of ndev-workflows. It is backwards compatible with napari-workflows, but is improved with yaml readability, safe loading, missing dependency warnings, and a clear spec-to-workflow interface. It includes the workflow widget from napari-ndev for viewer and batch processing of workflow files Copilot was used to help set up the architecture, but I did most of the work implementing the actual functionality. Unfortunately, I do not really have the understanding yet for WorkflowManager and UndoRedo to know why it exists, so for now its less validated. Copilot generated most of the tests, but they were at least eye ball checked.
2 parents 27b0b80 + 18bc7f5 commit 7ff5ad6

30 files changed

Lines changed: 4647 additions & 39 deletions

.github/workflows/test_and_deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
fail-fast: false
3131
matrix:
3232
platform: [ubuntu-latest, windows-latest, macos-latest]
33-
python-version: ["3.10", "3.11", "3.12", "3.13"]
33+
python-version: ["3.11", "3.12", "3.13"]
3434

3535
steps:
3636
- uses: actions/checkout@v6

.pre-commit-config.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ repos:
77
- id: trailing-whitespace
88
exclude: ^\.napari-hub/.*
99
- id: check-yaml # checks for correct yaml syntax for github actions ex.
10+
exclude: |
11+
(?x)(
12+
|^tests/resources/Workflow/workflows/.*\.yaml$
13+
)
1014
- repo: https://github.com/astral-sh/ruff-pre-commit
1115
rev: v0.14.10
1216
hooks:

README.md

Lines changed: 126 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,26 @@
99
[![npe2](https://img.shields.io/badge/plugin-npe2-blue?link=https://napari.org/stable/plugins/index.html)](https://napari.org/stable/plugins/index.html)
1010
[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-purple.json)](https://github.com/copier-org/copier)
1111

12-
reproducible processing workflows with napari
12+
**Reproducible processing workflows with napari**
1313

14-
----------------------------------
14+
A re-implementation of [napari-workflows](https://github.com/haesleinhuepf/napari-workflows) with backwards compatibility.
1515

16-
This [napari] plugin was generated with [copier] using the [napari-plugin-template] (main).
16+
---
1717

18-
<!--
19-
Don't miss the full getting started guide to set up your new package:
20-
https://github.com/napari/napari-plugin-template#getting-started
18+
This [napari] plugin was generated with [copier] using the [napari-plugin-template] (2.0.1).
2119

22-
and review the napari docs for plugin developers:
23-
https://napari.org/stable/plugins/index.html
24-
-->
20+
## What is ndev-workflows?
2521

26-
## Installation
22+
`ndev-workflows` is the workflow backend for napari image processing pipelines. It's a **drop-in replacement** for [napari-workflows](https://github.com/haesleinhuepf/napari-workflows) by Robert Haase, with these key improvements:
23+
24+
- **Safe YAML loading** — Uses `yaml.safe_load()` (no arbitrary code execution)
25+
- **Backwards compatible** — Automatically loads and migrates legacy napari-workflows files, and detects missing dependencies
26+
- **Same API** — Most code works without changes
27+
- **Future-ready** — Designed for upcoming npe2 workflow contributions (WIP), without relying on npe1, napari-time-slicer, and napari-tools-menu for interactivity
28+
29+
---
2730

28-
You can install `ndev-workflows` via [pip]:
31+
## Installation
2932

3033
```bash
3134
pip install ndev-workflows
@@ -37,41 +40,132 @@ If napari is not already installed, you can install `ndev-workflows` with napari
3740
pip install "ndev-workflows[all]"
3841
```
3942

43+
---
44+
45+
## Quick Start
46+
47+
```python
48+
from ndev_workflows import Workflow, save_workflow, load_workflow
49+
from skimage.filters import gaussian
50+
51+
# Create workflow
52+
workflow = Workflow()
53+
workflow.set("blurred", gaussian, "input_image", sigma=2.0)
54+
workflow.set("input_image", my_image)
55+
56+
# Execute
57+
result = workflow.get("blurred")
58+
59+
# Save
60+
save_workflow("pipeline.yaml", workflow, name="My Pipeline")
61+
62+
# Load and reuse
63+
loaded = load_workflow("pipeline.yaml")
64+
loaded.set("input_image", new_image)
65+
result = loaded.get("blurred")
66+
```
67+
68+
---
69+
70+
## YAML Format
71+
72+
Saved workflows use a safe, human-readable format:
73+
74+
```yaml
75+
name: Nucleus Segmentation
76+
description: Gaussian blur and thresholding
77+
modified: '2025-12-22'
78+
79+
inputs:
80+
- raw_image
81+
82+
outputs:
83+
- labels
84+
85+
tasks:
86+
blurred:
87+
function: skimage.filters.gaussian
88+
params:
89+
arg0: raw_image
90+
sigma: 2.0
91+
92+
labels:
93+
function: skimage.measure.label
94+
params:
95+
arg0: blurred
96+
```
97+
98+
**Key features:**
99+
100+
- No `!python/object` tags (safe to share)
101+
- Functions imported by module path
102+
- Params use `arg0`, `arg1`, etc. for positional args and keyword names for kwargs
40103

41-
To install latest development version:
104+
**Legacy format**: Old napari-workflows YAML files are automatically detected and migrated when loaded.
105+
106+
---
107+
108+
## Important Notes
109+
110+
### Function Dependencies
111+
112+
⚠️ Workflows **don't bundle functions** — they only store module paths. Recipients need the same packages installed.
113+
114+
If loading fails with `WorkflowNotRunnableError`, install the missing package:
42115

43116
```bash
44-
pip install git+https://github.com/ndev-kit/ndev-workflows.git
117+
pip install scikit-image # for skimage functions
118+
pip install napari-segment-blobs-and-things-with-membranes # for that plugin
45119
```
46120

121+
### Lazy Loading
122+
123+
Inspect workflows without importing functions:
124+
125+
```python
126+
workflow = load_workflow("untrusted.yaml", lazy=True)
127+
print(workflow.tasks) # Safe - doesn't execute
128+
```
129+
130+
---
131+
132+
## Integration
133+
134+
### Front-end plugins for interactive workflow building:
47135

136+
- [napari-assistant](https://github.com/haesleinhuepf/napari-assistant)
137+
- [napari-workflow-optimizer](https://github.com/haesleinhuepf/napari-workflow-optimizer)
138+
- [napari-workflow-inspector](https://github.com/haesleinhuepf/napari-workflow-inspector)
139+
140+
### Works with processing plugins:
141+
142+
- [napari-segment-blobs-and-things-with-membranes](https://www.napari-hub.org/plugins/napari-segment-blobs-and-things-with-membranes)
143+
- [pyclesperanto](https://github.com/clesperanto/napari_pyclesperanto_assistant)
144+
- And more!
145+
146+
---
48147

49148
## Contributing
50149

51-
Contributions are very welcome. Tests can be run with [tox], please ensure
52-
the coverage at least stays the same before you submit a pull request.
150+
```bash
151+
git clone https://github.com/ndev-kit/ndev-workflows.git
152+
cd ndev-workflows
153+
uv venv
154+
.venv\Scripts\activate
155+
uv pip install -e . --group dev
156+
pytest
157+
```
158+
159+
---
53160

54161
## License
55162

56163
Distributed under the terms of the [BSD-3] license,
57164
"ndev-workflows" is free and open source software
165+
Fork of [napari-workflows](https://github.com/haesleinhuepf/napari-workflows) by Robert Haase.
58166

59-
## Issues
60-
61-
If you encounter any problems, please [file an issue] along with a detailed description.
167+
---
62168

63-
[napari]: https://github.com/napari/napari
64-
[copier]: https://copier.readthedocs.io/en/stable/
65-
[MIT]: http://opensource.org/licenses/MIT
66-
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
67-
[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt
68-
[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt
69-
[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0
70-
[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt
71-
[napari-plugin-template]: https://github.com/napari/napari-plugin-template
72-
73-
[file an issue]: https://github.com/ndev-kit/ndev-workflows/issues
169+
## Issues
74170

75-
[tox]: https://tox.readthedocs.io/en/latest/
76-
[pip]: https://pypi.org/project/pip/
77-
[PyPI]: https://pypi.org/
171+
[File an issue](https://github.com/ndev-kit/ndev-workflows/issues) with your environment details, YAML file (if applicable), and error messages.

pyproject.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,25 @@ classifiers = [
1717
"Programming Language :: Python",
1818
"Programming Language :: Python :: 3",
1919
"Programming Language :: Python :: 3 :: Only",
20-
"Programming Language :: Python :: 3.10",
2120
"Programming Language :: Python :: 3.11",
2221
"Programming Language :: Python :: 3.12",
2322
"Programming Language :: Python :: 3.13",
2423
"Topic :: Scientific/Engineering :: Image Processing",
2524
]
26-
requires-python = ">=3.10"
25+
requires-python = ">=3.11" # ndevio requires 3.11+
2726
# napari can be included in dependencies if napari imports are required.
2827
# However, you should not include napari[all], napari[qt],
2928
# or any other Qt bindings directly (e.g. PyQt5, PySide2).
3029
# See best practices: https://napari.org/stable/plugins/building_a_plugin/best_practices.html
3130
dependencies = [
31+
"napari",
32+
"nbatch>=0.0.4",
33+
"ndevio>=0.6.0",
34+
"magicgui",
35+
"magic-class",
3236
"numpy",
37+
"dask",
38+
"pyyaml",
3339
]
3440

3541
[project.optional-dependencies]
@@ -42,6 +48,9 @@ dev = [
4248
"tox-uv",
4349
"pytest", # https://docs.pytest.org/en/latest/contents.html
4450
"pytest-cov", # https://pytest-cov.readthedocs.io/en/latest/
51+
"pytest-qt",
52+
"napari[pyqt6]",
53+
"napari-segment-blobs-and-things-with-membranes", # TODO: adds 76 transient dependencies, yuck. currently for legacy/current sample test workflows. will try to remove in future
4554
]
4655

4756
[project.entry-points."napari.manifest"]

src/ndev_workflows/__init__.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,35 @@
1+
"""ndev-workflows: Reproducible processing workflows with napari.
2+
3+
This package provides workflow management and batch processing for napari.
4+
It is a fork of napari-workflows by Robert Haase (BSD-3-Clause license),
5+
enhanced with:
6+
- Safe YAML loading (no arbitrary code execution)
7+
- Human-readable workflow format
8+
- Integration with ndev-settings and nbatch
9+
- npe2-native plugin architecture
10+
11+
Example
12+
-------
13+
>>> from ndev_workflows import Workflow, save_workflow, load_workflow
14+
>>> w = Workflow()
15+
>>> w.set("blurred", gaussian, "input", sigma=2.0)
16+
>>> save_workflow("my_workflow.yaml", w, name="My Pipeline")
17+
>>>
18+
>>> loaded = load_workflow("my_workflow.yaml")
19+
>>> loaded.set("input", image_data)
20+
>>> result = loaded.get("blurred")
21+
"""
22+
123
try:
224
from ._version import version as __version__
325
except ImportError:
426
__version__ = 'unknown'
527

28+
from ._io import load_workflow, save_workflow
29+
from ._workflow import Workflow
630

7-
__all__ = ()
31+
__all__ = [
32+
'Workflow',
33+
'load_workflow',
34+
'save_workflow',
35+
]

src/ndev_workflows/_batch.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Batch-processing helpers for ndev-workflows."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from nbatch import batch
8+
9+
10+
@batch(on_error='continue')
11+
def process_workflow_file(
12+
image_file: Path,
13+
result_dir: Path,
14+
workflow_file: Path,
15+
root_index_list: list[int],
16+
task_names: list[str],
17+
keep_original_images: bool,
18+
root_list: list[str],
19+
squeezed_img_dims: str,
20+
) -> Path:
21+
"""Process a single image file through a workflow.
22+
23+
Loads a fresh workflow instance per file for thread safety.
24+
25+
Parameters
26+
----------
27+
image_file : Path
28+
Path to the image file to process.
29+
result_dir : Path
30+
Directory to save results.
31+
workflow_file : Path
32+
Path to the workflow YAML file.
33+
root_index_list : list[int]
34+
Indices of channels to use as workflow roots.
35+
task_names : list[str]
36+
Names of workflow tasks to execute.
37+
keep_original_images : bool
38+
Whether to concatenate original images with results.
39+
root_list : list[str]
40+
Names of root channels (for output naming).
41+
squeezed_img_dims : str
42+
Squeezed dimension order of the image.
43+
44+
Returns
45+
-------
46+
Path
47+
Path to the saved output file.
48+
"""
49+
import dask.array as da
50+
import numpy as np
51+
from bioio.writers import OmeTiffWriter
52+
from bioio_base import transforms
53+
from ndevio import nImage
54+
55+
from ._io import load_workflow
56+
from ._spec import ensure_runnable
57+
58+
workflow = load_workflow(workflow_file, lazy=True)
59+
workflow = ensure_runnable(workflow)
60+
61+
img = nImage(image_file)
62+
63+
# Capture roots before modifying workflow (stable list of graph inputs)
64+
root_names = workflow.roots()
65+
66+
root_stack = []
67+
for idx, root_index in enumerate(root_index_list):
68+
if 'S' in img.dims.order:
69+
root_img = img.get_image_data('TSZYX', S=root_index)
70+
else:
71+
root_img = img.get_image_data('TCZYX', C=root_index)
72+
73+
root_stack.append(root_img)
74+
workflow.set(name=root_names[idx], func_or_data=np.squeeze(root_img))
75+
76+
result = workflow.get(name=task_names)
77+
78+
result_stack = np.asarray(result)
79+
result_stack = transforms.reshape_data(
80+
data=result_stack,
81+
given_dims='C' + squeezed_img_dims,
82+
return_dims='TCZYX',
83+
)
84+
85+
if result_stack.dtype == np.int64:
86+
result_stack = result_stack.astype(np.int32)
87+
88+
if keep_original_images:
89+
dask_images = da.concatenate(root_stack, axis=1) # along "C"
90+
result_stack = da.concatenate([dask_images, result_stack], axis=1)
91+
result_names = root_list + task_names
92+
else:
93+
result_names = task_names
94+
95+
output_path = result_dir / (image_file.stem + '.tiff')
96+
OmeTiffWriter.save(
97+
data=result_stack,
98+
uri=output_path,
99+
dim_order='TCZYX',
100+
channel_names=result_names,
101+
image_name=image_file.stem,
102+
physical_pixel_sizes=img.physical_pixel_sizes,
103+
)
104+
105+
return output_path

0 commit comments

Comments
 (0)