Skip to content

Commit 2a13ee3

Browse files
authored
Raise informative error on Zarr V2 parsing with Zarr-Python<3.1.3 (#829)
* Add back supports_partial_writes property, returning False * Raise informative error if using Zarr V2 parser with Zarr-Python<3.1.3 * Add test * Reduce code duplication
1 parent 61ca10b commit 2a13ee3

File tree

2 files changed

+67
-15
lines changed

2 files changed

+67
-15
lines changed

virtualizarr/parsers/zarr.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,14 @@ async def get_chunk_mapping(
180180
def get_metadata(self, zarr_array: ZarrArrayType) -> ArrayV3Metadata:
181181
"""Convert V2 metadata to V3 format."""
182182
from zarr.core.metadata import ArrayV2Metadata
183-
from zarr.metadata.migrate_v3 import _convert_array_metadata
183+
184+
try:
185+
from zarr.metadata.migrate_v3 import _convert_array_metadata
186+
except (ImportError, AttributeError):
187+
raise ImportError(
188+
f"Zarr-Python>=3.1.3 is required for parsing Zarr V2 into Zarr V3. "
189+
f"Found Zarr version '{zarr.__version__}'"
190+
)
184191

185192
v2_metadata = zarr_array.metadata
186193
assert isinstance(v2_metadata, ArrayV2Metadata)

virtualizarr/tests/test_parsers/test_zarr.py

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import xarray as xr
44
import zarr
55
from obstore.store import LocalStore
6+
from packaging import version
67
from zarr.api.asynchronous import open_array
78

89
from virtualizarr import open_virtual_dataset
@@ -13,18 +14,36 @@
1314

1415
ZarrArrayType = zarr.AsyncArray | zarr.Array
1516

17+
SKIP_OLDER_ZARR_PYTHON = pytest.mark.skipif(
18+
version.parse(zarr.__version__) < version.parse("3.1.3"),
19+
reason="Zarr V2 requires zarr>=3.1.3",
20+
)
1621

17-
@pytest.mark.parametrize(
18-
"zarr_store",
19-
[
20-
pytest.param(
21-
2,
22-
id="Zarr V2",
23-
),
24-
pytest.param(3, id="Zarr V3"),
25-
],
26-
indirect=True,
22+
SKIP_NEWER_ZARR_PYTHON = pytest.mark.skipif(
23+
version.parse(zarr.__version__) >= version.parse("3.1.3"),
24+
reason="Test only relevant for zarr<3.1.3",
2725
)
26+
27+
28+
def zarr_versions(param_name="zarr_format", indirect=False):
29+
"""
30+
Reusable parametrize decorator for Zarr V2 and V3 versions.
31+
32+
Args:
33+
param_name: Name of the parameter ('zarr_format' or 'zarr_store')
34+
indirect: Whether to use indirect parametrization (True for fixtures)
35+
"""
36+
return pytest.mark.parametrize(
37+
param_name,
38+
[
39+
pytest.param(2, id="Zarr V2", marks=SKIP_OLDER_ZARR_PYTHON),
40+
pytest.param(3, id="Zarr V3"),
41+
],
42+
indirect=indirect,
43+
)
44+
45+
46+
@zarr_versions(param_name="zarr_store", indirect=True)
2847
class TestOpenVirtualDatasetZarr:
2948
def test_loadable_variables(self, zarr_store, loadable_variables=["time", "air"]):
3049
# check loadable variables
@@ -106,7 +125,7 @@ def test_virtual_dataset_zarr_attrs(self, zarr_store):
106125
assert list(expected["dimension_names"]) == list(vds[array].dims)
107126

108127

109-
@pytest.mark.parametrize("zarr_format", [2, 3])
128+
@zarr_versions()
110129
def test_scalar_chunk_mapping(tmpdir, zarr_format):
111130
"""Test that scalar arrays produce correct chunk mappings for both V2 and V3."""
112131
import asyncio
@@ -153,7 +172,7 @@ def test_unsupported_zarr_format():
153172
get_strategy(mock_array)
154173

155174

156-
@pytest.mark.parametrize("zarr_format", [2, 3])
175+
@zarr_versions()
157176
def test_empty_array_chunk_mapping(tmpdir, zarr_format):
158177
"""Test chunk mapping for arrays with no chunks written yet."""
159178
import asyncio
@@ -178,6 +197,7 @@ async def get_chunk_map():
178197
assert chunk_map == {}
179198

180199

200+
@SKIP_OLDER_ZARR_PYTHON
181201
def test_v2_metadata_without_dimensions():
182202
"""Test V2 metadata conversion when array has no _ARRAY_DIMENSIONS attribute."""
183203
import asyncio
@@ -199,6 +219,30 @@ async def get_meta():
199219
assert len(metadata.dimension_names) == 2
200220

201221

222+
@SKIP_NEWER_ZARR_PYTHON
223+
def test_v2_metadata_raises_import_error_on_old_zarr():
224+
"""Test that V2 metadata conversion raises ImportError with zarr<3.1.3."""
225+
import asyncio
226+
227+
# Create a V2 array without dimension attributes
228+
store = zarr.storage.MemoryStore()
229+
_ = zarr.create(
230+
shape=(5, 10), chunks=(5, 5), dtype="int32", store=store, zarr_format=2
231+
)
232+
233+
async def get_meta():
234+
zarr_array = await open_array(store=store, mode="r")
235+
return get_metadata(zarr_array)
236+
237+
# Should raise ImportError with helpful message
238+
with pytest.raises(
239+
ImportError,
240+
match=r"Zarr-Python>=3\.1\.3 is required for parsing Zarr V2 into Zarr V3.*Found Zarr version",
241+
):
242+
asyncio.run(get_meta())
243+
244+
245+
@SKIP_OLDER_ZARR_PYTHON
202246
def test_v2_metadata_with_dimensions():
203247
"""Test V2 metadata conversion when array has _ARRAY_DIMENSIONS attribute."""
204248
import asyncio
@@ -219,6 +263,7 @@ async def get_meta():
219263
assert metadata.dimension_names == ("x", "y")
220264

221265

266+
@SKIP_OLDER_ZARR_PYTHON
222267
def test_v2_metadata_with_none_fill_value():
223268
"""Test V2 metadata conversion when fill_value is None."""
224269
import asyncio
@@ -260,7 +305,7 @@ async def get_manifest():
260305
assert manifest.shape_chunk_grid == (2, 2) # 10/5 = 2 chunks per dimension
261306

262307

263-
@pytest.mark.parametrize("zarr_format", [2, 3])
308+
@zarr_versions()
264309
def test_sparse_array_with_missing_chunks(tmpdir, zarr_format):
265310
"""Test that arrays with some missing chunks (sparse arrays) are handled correctly.
266311
@@ -317,7 +362,7 @@ async def get_manifest():
317362
assert manifest.shape_chunk_grid == (3, 3), "Chunk grid should be 3x3"
318363

319364

320-
@pytest.mark.parametrize("zarr_format", [2, 3])
365+
@zarr_versions()
321366
def test_parser_roundtrip_matches_xarray(tmpdir, zarr_format):
322367
"""Roundtrip a small dataset through the ZarrParser and compare with xarray."""
323368
import numpy as _np

0 commit comments

Comments
 (0)