33import xarray as xr
44import zarr
55from obstore .store import LocalStore
6+ from packaging import version
67from zarr .api .asynchronous import open_array
78
89from virtualizarr import open_virtual_dataset
1314
1415ZarrArrayType = 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 )
2847class 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 ( )
110129def 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 ( )
157176def 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
181201def 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
202246def 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
222267def 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 ( )
264309def 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 ( )
321366def 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