Skip to content

Commit 702f3c4

Browse files
committed
Add computed fields for requires_zarr and requires_plate
1 parent e180ef5 commit 702f3c4

5 files changed

Lines changed: 271 additions & 5 deletions

File tree

.github/workflows/test.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: windows-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: prefix-dev/setup-pixi@v0.8.0
13+
with:
14+
pixi-version: latest
15+
- run: pixi run test

pixi.lock

Lines changed: 51 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ dependencies = [
1717
"pyyaml>=6.0.3"
1818
]
1919

20+
[project.optional-dependencies]
21+
test = ["pytest>=8.0.0"]
22+
23+
[tool.pytest.ini_options]
24+
testpaths = ["tests"]
25+
2026
[project.scripts]
2127
biomero-schema = "biomero_schema.cli:cli"
2228

@@ -32,10 +38,12 @@ platforms = ["osx-64", "win-64"]
3238

3339
[tool.pixi.pypi-dependencies]
3440
biomero-schema = { path = ".", editable = true }
41+
pytest = ">=8.0.0"
3542

3643
[tool.pixi.tasks]
3744
json-schema = { cmd = ["biomero-schema", "schema"], description = "Print the json schema generated from pydantic model" }
3845
test-parse = { cmd = ["biomero-schema", "parse", "tests/example_workflow.json"], description = "Parse and run pydantic validation on test file" }
3946
test-pparse = { cmd = ["biomero-schema", "parse", "tests/example_workflow.json", "--pretty"], description = "Parse, validate, and show detailed info on test file" }
4047
test-jparse = { cmd = ["biomero-schema", "parse", "tests/example_workflow.json", "--json"], description = "Parse, validate, and show test file json contents" }
4148
test-validate = { cmd = ["biomero-schema", "validate", "tests/example_workflow.json"], description = "Run json schema validation on test file" }
49+
test = { cmd = ["pytest", "-v"], description = "Run all tests" }

src/biomero_schema/models.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""Pydantic models for the workflow schema."""
22
from typing import List, Optional, Union, Literal
3-
from pydantic import BaseModel, Field
3+
from pydantic import BaseModel, Field, computed_field, ConfigDict
4+
5+
_ZARR_FORMATS = frozenset({'zarr', 'omezarr', 'ome.zarr', 'ome-zarr'})
46

57

68
class Author(BaseModel):
@@ -133,6 +135,37 @@ class WorkflowSchema(BaseModel):
133135
outputs: List[OutputParameter] = Field([], description="List of output parameter descriptors")
134136
command_line: str = Field(..., alias="command-line", description="Command line template")
135137

136-
class Config:
137-
populate_by_name = True
138-
validate_by_name = True
138+
@computed_field(alias="requires-zarr")
139+
@property
140+
def requires_zarr(self) -> bool:
141+
"""True when any image input uses a ZARR format or has plate subtype."""
142+
for inp in self.inputs:
143+
if inp.type != 'image':
144+
continue
145+
fmt = inp.format or []
146+
if isinstance(fmt, str):
147+
fmt = [fmt]
148+
if any(f in _ZARR_FORMATS for f in fmt):
149+
return True
150+
sub = inp.sub_type or []
151+
if isinstance(sub, str):
152+
sub = [sub]
153+
if 'plate' in sub:
154+
return True
155+
return False
156+
157+
@computed_field(alias="requires-plate")
158+
@property
159+
def requires_plate(self) -> bool:
160+
"""True when any image input has plate subtype."""
161+
for inp in self.inputs:
162+
if inp.type != 'image':
163+
continue
164+
sub = inp.sub_type or []
165+
if isinstance(sub, str):
166+
sub = [sub]
167+
if 'plate' in sub:
168+
return True
169+
return False
170+
171+
model_config = ConfigDict(populate_by_name=True, validate_by_alias=True)

tests/test_computed_fields.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Tests for WorkflowSchema.requires_zarr and requires_plate computed fields."""
2+
import json
3+
import pytest
4+
from pathlib import Path
5+
from biomero_schema.models import WorkflowSchema
6+
7+
8+
TESTS_DIR = Path(__file__).parent
9+
10+
11+
def load_example() -> dict:
12+
return json.loads((TESTS_DIR / "example_workflow.json").read_text())
13+
14+
15+
def make_schema(image_inputs: list) -> WorkflowSchema:
16+
"""Build a minimal WorkflowSchema with the given image input list."""
17+
base = load_example()
18+
base["inputs"] = image_inputs
19+
return WorkflowSchema.model_validate(base)
20+
21+
22+
def image_input(fmt=None, subtype=None) -> dict:
23+
inp = {
24+
"id": "input_image",
25+
"type": "image",
26+
"name": "Input",
27+
"value-key": "[INPUT_IMAGE]",
28+
"command-line-flag": "--input",
29+
}
30+
if fmt is not None:
31+
inp["format"] = fmt
32+
if subtype is not None:
33+
inp["sub-type"] = subtype
34+
return inp
35+
36+
37+
# ---------------------------------------------------------------------------
38+
# requires_zarr
39+
# ---------------------------------------------------------------------------
40+
41+
class TestRequiresZarr:
42+
def test_false_for_tiff(self):
43+
schema = make_schema([image_input(fmt="tif")])
44+
assert schema.requires_zarr is False
45+
46+
def test_false_for_ometiff(self):
47+
schema = make_schema([image_input(fmt="ometiff")])
48+
assert schema.requires_zarr is False
49+
50+
def test_false_for_png(self):
51+
schema = make_schema([image_input(fmt="png")])
52+
assert schema.requires_zarr is False
53+
54+
def test_false_for_no_image_inputs(self):
55+
base = load_example()
56+
base["inputs"] = [
57+
{"id": "x", "type": "float", "value-key": "[X]", "command-line-flag": "--x"}
58+
]
59+
schema = WorkflowSchema.model_validate(base)
60+
assert schema.requires_zarr is False
61+
62+
def test_true_for_zarr(self):
63+
schema = make_schema([image_input(fmt="zarr")])
64+
assert schema.requires_zarr is True
65+
66+
def test_true_for_omezarr(self):
67+
schema = make_schema([image_input(fmt="omezarr")])
68+
assert schema.requires_zarr is True
69+
70+
def test_true_for_ome_zarr_dot(self):
71+
schema = make_schema([image_input(fmt="ome.zarr")])
72+
assert schema.requires_zarr is True
73+
74+
def test_true_for_ome_zarr_dash(self):
75+
schema = make_schema([image_input(fmt="ome-zarr")])
76+
assert schema.requires_zarr is True
77+
78+
def test_true_for_format_list_containing_zarr(self):
79+
schema = make_schema([image_input(fmt=["tif", "zarr"])])
80+
assert schema.requires_zarr is True
81+
82+
def test_true_when_plate_subtype(self):
83+
"""Plate implies ZARR."""
84+
schema = make_schema([image_input(fmt="omezarr", subtype="plate")])
85+
assert schema.requires_zarr is True
86+
87+
def test_false_for_zarr_format_on_non_image_type(self):
88+
"""format field on a non-image input should not trigger zarr."""
89+
base = load_example()
90+
base["inputs"] = [
91+
{"id": "x", "type": "file", "format": "zarr", "value-key": "[X]", "command-line-flag": "--x"}
92+
]
93+
schema = WorkflowSchema.model_validate(base)
94+
assert schema.requires_zarr is False
95+
96+
97+
# ---------------------------------------------------------------------------
98+
# requires_plate
99+
# ---------------------------------------------------------------------------
100+
101+
class TestRequiresPlate:
102+
def test_false_by_default(self):
103+
schema = make_schema([image_input(fmt="tif", subtype="grayscale")])
104+
assert schema.requires_plate is False
105+
106+
def test_false_for_no_subtype(self):
107+
schema = make_schema([image_input(fmt="zarr")])
108+
assert schema.requires_plate is False
109+
110+
def test_true_for_plate_subtype(self):
111+
schema = make_schema([image_input(fmt="omezarr", subtype="plate")])
112+
assert schema.requires_plate is True
113+
114+
def test_true_for_plate_in_subtype_list(self):
115+
schema = make_schema([image_input(fmt="omezarr", subtype=["plate"])])
116+
assert schema.requires_plate is True
117+
118+
def test_false_for_other_subtype(self):
119+
schema = make_schema([image_input(fmt="tif", subtype="grayscale")])
120+
assert schema.requires_plate is False
121+
122+
def test_plate_also_sets_zarr(self):
123+
"""Plate always implies requires_zarr too."""
124+
schema = make_schema([image_input(fmt="omezarr", subtype="plate")])
125+
assert schema.requires_zarr is True
126+
assert schema.requires_plate is True
127+
128+
129+
# ---------------------------------------------------------------------------
130+
# Serialisation — flags appear in model_dump(by_alias=True)
131+
# ---------------------------------------------------------------------------
132+
133+
class TestSerialisedOutput:
134+
def test_keys_present_in_dump(self):
135+
schema = make_schema([image_input(fmt="zarr", subtype="plate")])
136+
data = schema.model_dump(by_alias=True)
137+
assert "requires-zarr" in data
138+
assert "requires-plate" in data
139+
140+
def test_values_correct_in_dump(self):
141+
schema = make_schema([image_input(fmt="zarr", subtype="plate")])
142+
data = schema.model_dump(by_alias=True)
143+
assert data["requires-zarr"] is True
144+
assert data["requires-plate"] is True
145+
146+
def test_false_values_in_dump(self):
147+
schema = make_schema([image_input(fmt="tif")])
148+
data = schema.model_dump(by_alias=True)
149+
assert data["requires-zarr"] is False
150+
assert data["requires-plate"] is False
151+
152+
153+
# ---------------------------------------------------------------------------
154+
# Round-trip through example_workflow.json (tiff input → both False)
155+
# ---------------------------------------------------------------------------
156+
157+
def test_example_workflow_no_zarr():
158+
schema = WorkflowSchema.model_validate(load_example())
159+
assert schema.requires_zarr is False
160+
assert schema.requires_plate is False

0 commit comments

Comments
 (0)