Skip to content

Commit 5f401b1

Browse files
authored
Add a compatability patch for OME-Zarr <=0.2 (#56)
# References and relevant issues Closes #50 # Description This PR creates a model for checking data and doing compatability checks. In this case, old v0.1 and 0.2 ome-zarr metadata does not contain CoordinateTransforms for the scale, which BioImage tries to access. This will raise a `KeyError` and then we gracefully just create default scale and units, as would occur if there was no BioImage dimensions. I opted to go the logging route because it seems napari's NotificationManager only raises one warning at a time, and its presently overwritten by a Zarr error. Logging will always land it somewhere, even if that is the command line :( I implemented as low-overhead of a check as possible by checking against the name of the imported reader module. While these checks _could_ live in `_napari_reader`, I opted for nImage because we _can_ patch this incompatability (for now). This does not gaurantee that all metadata of a v0.1 and v0.2 will work for the `BioImage` attributes, but for nImage's purpose this will work :)
1 parent e4d34db commit 5f401b1

File tree

4 files changed

+215
-4
lines changed

4 files changed

+215
-4
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Compatibility checks for known bioio reader format limitations.
2+
3+
These functions inspect reader state after initialisation and emit a single
4+
consolidated warning when a known incompatibility is detected, so that
5+
downstream property accessors can fail silently without repeating noisy
6+
messages.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import logging
12+
from typing import TYPE_CHECKING
13+
14+
logger = logging.getLogger(__name__)
15+
16+
if TYPE_CHECKING:
17+
from bioio_ome_zarr import Reader as OmeZarrReader
18+
19+
20+
def warn_if_old_zarr_format(reader: OmeZarrReader) -> None:
21+
"""Emit one warning if *reader* is a ``bioio_ome_zarr.Reader`` for a v0.1/v0.2 store.
22+
23+
``bioio_ome_zarr.Reader`` unconditionally accesses ``coordinateTransformations``
24+
inside each ``datasets`` entry — a key introduced in OME-Zarr v0.3. Any call
25+
to ``reader.scale`` or ``reader.dimension_properties`` therefore raises
26+
``KeyError`` for v0.1/v0.2 stores.
27+
28+
``nImage`` silently falls back to ``scale=1.0`` / ``units=None`` in those
29+
paths, but this warning gives users a single, clear explanation upfront via
30+
the napari activity log.
31+
32+
Parameters
33+
----------
34+
reader : OmeZarrReader
35+
"""
36+
multiscales = reader._multiscales_metadata
37+
datasets = multiscales[0].get('datasets', []) if multiscales else []
38+
if datasets and 'coordinateTransformations' not in datasets[0]:
39+
version = multiscales[0].get('version', 'unknown (likely 0.1 or 0.2)')
40+
logger.warning(
41+
'OME-Zarr compatibility warning: this store appears to be '
42+
'OME-Zarr spec version %s, which pre-dates '
43+
"'coordinateTransformations' in dataset entries (introduced "
44+
'in v0.3). Physical scale and unit metadata cannot be read. '
45+
'ndevio will open the image with scale=1.0 and no units. '
46+
'Consider converting the file to OME-Zarr >=v0.4.',
47+
version,
48+
)

src/ndevio/nimage.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,13 @@ def __init__(
181181
self.path = None
182182
self._is_remote = False
183183

184+
# Any compatibility warnings for old formats should be emitted at this point
185+
# Cheaply check without imports by looking at the reader's module name
186+
if self.reader.__module__.startswith('bioio_ome_zarr'):
187+
from .bioio_plugins._compatibility import warn_if_old_zarr_format
188+
189+
warn_if_old_zarr_format(self.reader)
190+
184191
@property
185192
def reference_xarray(self) -> xr.DataArray:
186193
"""Image data as xarray DataArray for metadata determination.
@@ -363,12 +370,13 @@ def layer_scale(self) -> tuple[float, ...]:
363370
axis_labels = self.layer_axis_labels
364371

365372
# Try to get scale from BioImage - may fail for array-like inputs
366-
# where physical_pixel_sizes is None
373+
# where physical_pixel_sizes is None (AttributeError), or for old OME-Zarr formats
374+
# (v0.1/v0.2) that lack 'coordinateTransformations' in their dataset
375+
# metadata (introduced in v0.3) (KeyError).
367376
try:
368377
bio_scale = self.scale
369-
except AttributeError:
378+
except (AttributeError, KeyError):
370379
return tuple(1.0 for _ in axis_labels)
371-
372380
return tuple(
373381
getattr(bio_scale, dim, None) or 1.0 for dim in axis_labels
374382
)
@@ -419,7 +427,9 @@ def layer_units(self) -> tuple[str | None, ...]:
419427

420428
try:
421429
dim_props = self.dimension_properties
422-
except AttributeError:
430+
# Old OME-Zarr v0.1/v0.2: dimension_properties → reader.scale →
431+
# _get_scale_array raises KeyError for missing 'coordinateTransformations'.
432+
except (AttributeError, KeyError):
423433
return tuple(None for _ in axis_labels)
424434

425435
def _get_unit(dim: str) -> str | None:
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Tests for bioio_plugins._compatibility module."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from unittest.mock import MagicMock, patch
7+
8+
9+
def _make_zarr_reader(multiscales: list) -> MagicMock:
10+
"""Return a mock that looks like a ``bioio_ome_zarr.Reader`` instance."""
11+
reader = MagicMock()
12+
reader.__module__ = 'bioio_ome_zarr.reader'
13+
reader._multiscales_metadata = multiscales
14+
return reader
15+
16+
17+
def _make_v01_multiscales(version: str = '0.1') -> list:
18+
"""Minimal OME-Zarr v0.1/v0.2 multiscales — no coordinateTransformations."""
19+
return [{'version': version, 'datasets': [{'path': '0'}, {'path': '1'}]}]
20+
21+
22+
def _make_v03_multiscales() -> list:
23+
"""Minimal OME-Zarr >=v0.3 multiscales — has coordinateTransformations."""
24+
return [
25+
{
26+
'version': '0.3',
27+
'datasets': [
28+
{
29+
'path': '0',
30+
'coordinateTransformations': [
31+
{'type': 'scale', 'scale': [1.0, 0.5, 0.5]}
32+
],
33+
}
34+
],
35+
}
36+
]
37+
38+
39+
class TestWarnIfOldZarrFormat:
40+
"""Unit tests for warn_if_old_zarr_format."""
41+
42+
def test_v01_emits_warning(self, caplog):
43+
"""v0.1 metadata (no coordinateTransformations) triggers a warning."""
44+
from ndevio.bioio_plugins._compatibility import warn_if_old_zarr_format
45+
46+
reader = _make_zarr_reader(_make_v01_multiscales('0.1'))
47+
48+
with caplog.at_level(
49+
logging.WARNING, logger='ndevio.bioio_plugins._compatibility'
50+
):
51+
warn_if_old_zarr_format(reader)
52+
53+
assert len(caplog.records) == 1
54+
assert '0.1' in caplog.records[0].message
55+
assert 'coordinateTransformations' in caplog.records[0].message
56+
assert 'scale=1.0' in caplog.records[0].message
57+
58+
def test_v02_emits_warning(self, caplog):
59+
"""v0.2 metadata also triggers a warning."""
60+
from ndevio.bioio_plugins._compatibility import warn_if_old_zarr_format
61+
62+
reader = _make_zarr_reader(_make_v01_multiscales('0.2'))
63+
64+
with caplog.at_level(
65+
logging.WARNING, logger='ndevio.bioio_plugins._compatibility'
66+
):
67+
warn_if_old_zarr_format(reader)
68+
69+
assert len(caplog.records) == 1
70+
assert '0.2' in caplog.records[0].message
71+
72+
def test_v03_no_warning(self, caplog):
73+
"""v0.3+ metadata (has coordinateTransformations) emits no warning."""
74+
from ndevio.bioio_plugins._compatibility import warn_if_old_zarr_format
75+
76+
reader = _make_zarr_reader(_make_v03_multiscales())
77+
78+
with caplog.at_level(
79+
logging.WARNING, logger='ndevio.bioio_plugins._compatibility'
80+
):
81+
warn_if_old_zarr_format(reader)
82+
83+
assert len(caplog.records) == 0
84+
85+
def test_empty_multiscales_no_warning(self, caplog):
86+
"""Empty multiscales list does not raise and emits no warning."""
87+
from ndevio.bioio_plugins._compatibility import warn_if_old_zarr_format
88+
89+
reader = _make_zarr_reader([])
90+
91+
with caplog.at_level(
92+
logging.WARNING, logger='ndevio.bioio_plugins._compatibility'
93+
):
94+
warn_if_old_zarr_format(reader)
95+
96+
assert len(caplog.records) == 0
97+
98+
def test_unknown_version_in_warning(self, caplog):
99+
"""When version key is missing the warning still fires with a fallback string."""
100+
from ndevio.bioio_plugins._compatibility import warn_if_old_zarr_format
101+
102+
# No 'version' key, no 'coordinateTransformations'
103+
multiscales = [{'datasets': [{'path': '0'}]}]
104+
reader = _make_zarr_reader(multiscales)
105+
106+
with caplog.at_level(
107+
logging.WARNING, logger='ndevio.bioio_plugins._compatibility'
108+
):
109+
warn_if_old_zarr_format(reader)
110+
111+
assert len(caplog.records) == 1
112+
assert 'unknown' in caplog.records[0].message.lower()
113+
114+
115+
class TestNImageCompatibilityGuard:
116+
"""Integration tests: nImage.__init__ only calls warn_if_old_zarr_format for zarr readers."""
117+
118+
def test_non_zarr_reader_skips_check(self, resources_dir):
119+
"""A TIFF-backed nImage never calls warn_if_old_zarr_format."""
120+
from ndevio import nImage
121+
122+
with patch(
123+
'ndevio.bioio_plugins._compatibility.warn_if_old_zarr_format'
124+
) as mock_check:
125+
nImage(resources_dir / 'cells3d2ch_legacy.tiff')
126+
127+
mock_check.assert_not_called()
128+
129+
def test_zarr_reader_calls_check(self, resources_dir):
130+
"""A zarr-backed nImage calls warn_if_old_zarr_format exactly once."""
131+
from ndevio import nImage
132+
133+
with patch(
134+
'ndevio.bioio_plugins._compatibility.warn_if_old_zarr_format'
135+
) as mock_check:
136+
nImage(resources_dir / 'dimension_handling_zyx_V3.zarr')
137+
138+
mock_check.assert_called_once()

tests/test_nimage.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,21 @@ def test_nImage_remote_zarr():
5353
assert img.reference_xarray.shape == (2, 512, 512)
5454

5555

56+
@pytest.mark.network
57+
def test_nImage_remote_zarr_old_format(caplog):
58+
"""Test that nImage emits a warning for old OME-Zarr formats when reading remotely."""
59+
remote_zarr = 'https://uk1s3.embassy.ebi.ac.uk/idr/zarr/v0.1/9836841.zarr' # from https://github.com/ndev-kit/ndevio/issues/50
60+
with caplog.at_level(
61+
'WARNING', logger='ndevio.bioio_plugins._compatibility'
62+
):
63+
img = nImage(remote_zarr)
64+
assert img.path == remote_zarr
65+
# should catch a key error due to old format
66+
# but still quietly create a scale with no units
67+
assert img.layer_scale == (1.0, 1.0)
68+
assert img.layer_units == (None, None)
69+
70+
5671
def test_nImage_ome_reader(resources_dir: Path):
5772
"""
5873
Test that the OME-TIFF reader is used for OME-TIFF files.

0 commit comments

Comments
 (0)