Tool Packaging and Versioning¶
BioImageFlow distributes tools as standard Python packages. The versioned loading system allows multiple versions of the same package to coexist in a single process, enabling reproducible workflows.
Package boundary rules¶
Use a package when a tool is reused across projects, distributed independently, versioned, or carries non-trivial dependencies or runtime assets. Keep package boundaries explicit:
A package owns its tools, reusable workflow factories, tests, fixtures, documentation, examples, and small runtime assets.
Package examples must import from that package.
Each public tool and workflow exported by the package must have tests.
ProcessingToolmodules may import only the standard library andbioimageflow-coreat module import time. Tool-specific dependencies are imported insideprocess_roworprocess_batch.DataFrameToolmodules belong to the main process and may importbioimageflow, pandas, and other main-process dependencies at module import time.
Package layout¶
A tool package is a standard Python package with a pyproject.toml:
For a practical package scaffold that includes package-owned docs and tests, see Creating a Custom Tool Package.
my_tools/
pyproject.toml
my_tools/
__init__.py
segmenter.py
loader.py
pipeline.py # optional build_workflow factory
utils/
__init__.py
filters.py
# pyproject.toml
[project]
name = "my-tools"
version = "1.0.0"
requires-python = ">=3.10"
dependencies = ["bioimageflow-core"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Relative imports are mandatory¶
All intra-package imports must be relative. This is the most important rule for tool packages.
# my_tools/__init__.py
# Correct -- always use relative imports within a tool package
from .segmenter import MySegmenter
from .utils.filters import apply_filter
# WRONG -- absolute imports break versioned loading
# from my_tools.segmenter import MySegmenter
Warning
Absolute intra-package imports (e.g., from my_tools.segmenter import X)
will silently resolve to whichever version of the package was loaded first,
mixing code from different versions. Always use relative imports.
Why this matters: BioImageFlow loads tool packages into isolated
namespaces so that multiple versions can coexist (e.g.,
my_tools__1_0_0 and my_tools__2_0_0). Relative imports resolve
within the correct scoped namespace. Absolute imports bypass the scoping
entirely.
This applies everywhere in the package:
__init__.pyTool modules importing from sibling modules
build_workflowfactories importing tool classesUtility modules importing from other utility modules
Example tool package¶
ProcessingTool¶
# my_tools/segmenter.py
from bioimageflow_core.tool import ProcessingTool, IOModel
from bioimageflow_core.environment import GENERAL_ENV
class MySegmenter(ProcessingTool):
display_name = "My Segmenter"
environment = GENERAL_ENV
class Inputs(IOModel):
image: str
threshold: float = 0.5
class Outputs(IOModel):
mask: str
def process_row(self, arguments):
# ... processing logic ...
return self.Outputs(mask=output_path)
Note that process_row must return a self.Outputs(...) instance, not a
raw dict. See Writing Custom Tools for more details on writing tools.
Tool-specific imports such as skimage or torch belong inside
process_row / process_batch so the package can be imported for schema
inspection without the worker environment installed.
Workflow factory¶
Workflow modules compose tools through an exact build_workflow factory.
They use relative imports for tools owned by the same package:
# my_tools/pipeline.py
from bioimageflow import Workflow
from .segmenter import MySegmenter # relative import
def build_workflow() -> Workflow:
workflow = Workflow(name="segment_pipeline", display_name="Segment Pipeline")
with workflow:
image = workflow.input("image", str, id="input-image")
threshold = workflow.input("threshold", float, default=0.5, id="input-threshold")
result = MySegmenter()(image=image, threshold=threshold, name="segment")
workflow.output("mask", result["mask"], id="output-mask")
return workflow
__init__.py¶
Re-export tools for convenient access:
# my_tools/__init__.py
from .segmenter import MySegmenter
from .loader import ImageLoader
from .pipeline import build_workflow
Testing requirements¶
Every package must ship tests with the package source. For each public tool, cover:
schema serialization for
InputsandOutputs;construction-time validation for valid and invalid bindings;
one successful execution path with tiny synthetic or committed public fixtures;
output template resolution and returned
Outputsinstances;expected failures for invalid parameters, missing files, or unsupported data.
For each public workflow factory, cover graph construction, public interface,
Workflow.to_dict / Workflow.from_dict round-trip when supported, and
one end-to-end execution on public or synthetic data. Tests should run
without network access unless the package explicitly tests a downloader, in
which case mock the network boundary.
Using versioned packages¶
Workflow-local custom tools do not need to become packages just to make
a workflow shareable: Workflow.export(path) embeds the workflow-local
tools/ package in the workflow JSON. Package a tool when it should be
reused by multiple workflows, distributed independently, versioned, or
installed with non-trivial assets and dependencies.
Loading a single version¶
For shareable workflow scripts, declare dependencies using
PEP 723 inline metadata.
require_tool_packages parses the metadata, installs missing packages,
and registers canonical names so standard imports work:
# /// script
# dependencies = [
# "my-tools==1.0.0",
# ]
# ///
from bioimageflow import Workflow, configure_wetlands, require_tool_packages
configure_wetlands(wetlands_instance_path="./wetlands")
require_tool_packages(__file__)
# Normal imports work after require_tool_packages
from my_tools import MySegmenter
with Workflow() as wf:
result = MySegmenter()(image=raw["path"])
wf.compute(result)
Loading multiple versions side by side¶
Use load_versioned_package and resolve_tool_class to load two
versions of the same package simultaneously:
from bioimageflow import Workflow, load_versioned_package
from bioimageflow.tool_loader import resolve_tool_class
from bioimageflow_common_tools import Concat
v1 = load_versioned_package("my_tools", "1.0.0")
v2 = load_versioned_package("my_tools", "2.0.0")
# Resolve distinct class objects from each version
SegV1 = resolve_tool_class("my_tools", "1.0.0", "my_tools.segmenter", "MySegmenter")
SegV2 = resolve_tool_class("my_tools", "2.0.0", "my_tools.segmenter", "MySegmenter")
assert SegV1 is not SegV2 # different classes
with Workflow() as wf:
result_v1 = SegV1()(image=raw["path"])
result_v2 = SegV2()(image=raw["path"])
merged = Concat()(result_v1, result_v2)
wf.compute(merged)
Cleanup:
from bioimageflow import unload_versioned_package
unload_versioned_package("my_tools", "1.0.0")
unload_versioned_package("my_tools", "2.0.0")
Tool store¶
Versioned packages are installed in the BioImageFlow tool store.
The path is resolved from BIOIMAGEFLOW_TOOL_STORE, then
BIOIMAGEFLOW_HOME / "tool_packages", then
~/.bioimageflow/tool_packages:
~/.bioimageflow/tool_packages/
my_tools/
1.0.0/
my_tools/
__init__.py
segmenter.py
...
2.0.0/
my_tools/
__init__.py
segmenter.py
...
Packages are installed automatically by require_tool_packages or
Workflow.load() when version info is present in a serialized workflow.
require_tool_packages uses Wetlands’ Pixi installation to run
pip install --target for missing packages. If a script needs a
project-local Wetlands directory, call configure_wetlands() before
require_tool_packages(); otherwise the default instance path is used.
Programmatic tool enumeration¶
For programmatic tool enumeration — populating a GUI tool palette, building a
plugin index, or wiring schema serializers into a host process — prefer
ToolRegistry over the lower-level load_versioned_package /
resolve_tool_class pair. ToolRegistry wraps both and exposes
register_package, get_class, get_metadata, and list_tools.
See ToolRegistry for the full surface.