11import math
22import sys
33from collections .abc import Callable , Mapping
4+ from contextlib import nullcontext
45from typing import Any , Literal
56
67import hypothesis .extra .numpy as npst
1516from zarr .codecs .bytes import BytesCodec
1617from zarr .core .array import Array
1718from zarr .core .chunk_key_encodings import DefaultChunkKeyEncoding
18- from zarr .core .common import JSON , ZarrFormat
19+ from zarr .core .common import JSON , AccessModeLiteral , ZarrFormat
1920from zarr .core .dtype import get_data_type_from_native_dtype
2021from zarr .core .metadata import ArrayV2Metadata , ArrayV3Metadata
21- from zarr .core .metadata .v3 import RegularChunkGridMetadata
22+ from zarr .core .metadata .v3 import RectilinearChunkGridMetadata , RegularChunkGridMetadata
2223from zarr .core .sync import sync
2324from zarr .storage import MemoryStore , StoreLike
2425from zarr .storage ._common import _dereference_path
@@ -140,11 +141,11 @@ def array_metadata(
140141 # separator = draw(st.sampled_from(['/', '\\']))
141142 shape = draw (array_shapes ())
142143 ndim = len (shape )
143- chunk_shape = draw (array_shapes (min_dims = ndim , max_dims = ndim , min_side = 1 ))
144144 np_dtype = draw (dtypes ())
145145 dtype = get_data_type_from_native_dtype (np_dtype )
146146 fill_value = draw (npst .from_dtype (np_dtype ))
147147 if zarr_format == 2 :
148+ chunk_shape = draw (array_shapes (min_dims = ndim , max_dims = ndim , min_side = 1 ))
148149 return ArrayV2Metadata (
149150 shape = shape ,
150151 chunks = chunk_shape ,
@@ -157,10 +158,11 @@ def array_metadata(
157158 compressor = None ,
158159 )
159160 else :
161+ chunk_grid = draw (chunk_grids (shape = shape ))
160162 return ArrayV3Metadata (
161163 shape = shape ,
162164 data_type = dtype ,
163- chunk_grid = RegularChunkGridMetadata ( chunk_shape = chunk_shape ) ,
165+ chunk_grid = chunk_grid ,
164166 fill_value = fill_value ,
165167 attributes = draw (attributes ), # type: ignore[arg-type]
166168 dimension_names = draw (dimension_names (ndim = ndim )),
@@ -255,6 +257,7 @@ def arrays(
255257 arrays : st .SearchStrategy | None = None ,
256258 attrs : st .SearchStrategy = attrs ,
257259 zarr_formats : st .SearchStrategy = zarr_formats ,
260+ open_mode : AccessModeLiteral = "w" ,
258261) -> AnyArray :
259262 store = draw (stores , label = "store" )
260263 path = draw (paths , label = "array parent" )
@@ -264,36 +267,61 @@ def arrays(
264267 if arrays is None :
265268 arrays = numpy_arrays (shapes = shapes )
266269 nparray = draw (arrays , label = "array data" )
267- chunk_shape = draw (chunk_shapes (shape = nparray .shape ), label = "chunk shape" )
268270 dim_names : None | list [str | None ] = None
271+
272+ # For v3 arrays, optionally use RectilinearChunkGridMetadata
273+ chunk_grid_meta : RegularChunkGridMetadata | RectilinearChunkGridMetadata | None = None
269274 shard_shape = None
270275 if zarr_format == 3 :
276+ chunk_grid_meta = draw (chunk_grids (shape = nparray .shape ), label = "chunk grid" )
277+
278+ # Sharding is only supported with regular chunk grids, and has complex
279+ # divisibility constraints that don't play well with hypothesis shrinking.
280+ # Disabled for now — sharding should be tested separately.
281+
271282 dim_names = draw (dimension_names (ndim = nparray .ndim ), label = "dimension names" )
272- if all (s > 0 for s in nparray .shape ) and all (c > 0 for c in chunk_shape ):
273- shard_shape = draw (
274- st .none () | shard_shapes (shape = nparray .shape , chunk_shape = chunk_shape ),
275- label = "shard shape" ,
276- )
283+ else :
284+ dim_names = None
285+
277286 # test that None works too.
278287 fill_value = draw (st .one_of ([st .none (), npst .from_dtype (nparray .dtype )]))
279288 # compressor = draw(compressors)
280289
281290 expected_attrs = {} if attributes is None else attributes
282291
283292 array_path = _dereference_path (path , name )
284- root = zarr .open_group (store , mode = "w" , zarr_format = zarr_format )
285-
286- a = root .create_array (
287- array_path ,
288- shape = nparray .shape ,
289- chunks = chunk_shape ,
290- shards = shard_shape ,
291- dtype = nparray .dtype ,
292- attributes = attributes ,
293- # compressor=compressor, # FIXME
294- fill_value = fill_value ,
295- dimension_names = dim_names ,
296- )
293+ root = zarr .open_group (store , mode = open_mode , zarr_format = zarr_format )
294+
295+ # Convert chunk grid metadata to a form create_array accepts:
296+ # - RegularChunkGridMetadata -> flat tuple of ints
297+ # - RectilinearChunkGridMetadata -> nested list of ints (triggers rectilinear path)
298+ # - v2 -> flat tuple of ints
299+ chunks_param : tuple [int , ...] | list [list [int ]]
300+ use_rectilinear = False
301+ if zarr_format == 3 and chunk_grid_meta is not None :
302+ if isinstance (chunk_grid_meta , RectilinearChunkGridMetadata ):
303+ chunks_param = [
304+ list (dim ) if isinstance (dim , tuple ) else [dim ]
305+ for dim in chunk_grid_meta .chunk_shapes
306+ ]
307+ use_rectilinear = True
308+ else :
309+ chunks_param = chunk_grid_meta .chunk_shape
310+ else :
311+ chunks_param = draw (chunk_shapes (shape = nparray .shape ), label = "chunk shape" )
312+
313+ with zarr .config .set ({"array.rectilinear_chunks" : True }) if use_rectilinear else nullcontext ():
314+ a = root .create_array (
315+ array_path ,
316+ shape = nparray .shape ,
317+ chunks = chunks_param ,
318+ shards = shard_shape ,
319+ dtype = nparray .dtype ,
320+ attributes = attributes ,
321+ # compressor=compressor, # FIXME
322+ fill_value = fill_value ,
323+ dimension_names = dim_names ,
324+ )
297325
298326 assert isinstance (a , Array )
299327 if a .metadata .zarr_format == 3 :
@@ -303,8 +331,16 @@ def arrays(
303331 assert a .name == "/" + a .path
304332 assert isinstance (root [array_path ], Array )
305333 assert nparray .shape == a .shape
306- assert chunk_shape == a .chunks
307- assert shard_shape == a .shards
334+
335+ # Verify chunks — for rectilinear grids, .chunks raises
336+ if zarr_format == 3 :
337+ if isinstance (a .metadata .chunk_grid , RectilinearChunkGridMetadata ):
338+ assert shard_shape is None
339+ else :
340+ assert isinstance (a .metadata .chunk_grid , RegularChunkGridMetadata )
341+ assert a .metadata .chunk_grid .chunk_shape == a .chunks
342+ assert shard_shape == a .shards
343+
308344 assert a .basename == name , (a .basename , name )
309345 assert dict (a .attrs ) == expected_attrs
310346
@@ -334,35 +370,83 @@ def simple_arrays(
334370def rectilinear_chunks (draw : st .DrawFn , * , shape : tuple [int , ...]) -> list [list [int ]]:
335371 """Generate valid rectilinear chunk shapes for a given array shape.
336372
337- Each dimension is partitioned into 1..min(size, 10) chunks by drawing
338- unique divider points within [1, size-1].
373+ Uses two modes per dimension:
374+ - "expanded": random divider points create arbitrary chunk sizes
375+ - "rle": uniform chunks with optional remainder, optionally shuffled
376+
377+ Keeps max chunks per dimension <= 20 to avoid performance issues
378+ in property tests. With higher dimensions, the total chunk count
379+ grows multiplicatively.
339380 """
340381 chunk_shapes : list [list [int ]] = []
341382 for size in shape :
342383 assert size > 0
343- max_chunks = min (size , 10 )
344- nchunks = draw (st .integers (min_value = 1 , max_value = max_chunks ))
345- if nchunks == 1 :
346- chunk_shapes .append ([size ])
347- else :
348- dividers = sorted (
349- draw (
350- st .lists (
351- st .integers (min_value = 1 , max_value = size - 1 ),
352- min_size = nchunks - 1 ,
353- max_size = nchunks - 1 ,
354- unique = True ,
384+ if size > 1 :
385+ mode = draw (st .sampled_from (["expanded" , "rle" ]))
386+ if mode == "expanded" :
387+ event ("rectilinear expanded" )
388+ max_chunks = min (size - 1 , 20 )
389+ nchunks = draw (st .integers (min_value = 1 , max_value = max_chunks ))
390+ dividers = sorted (
391+ draw (
392+ st .lists (
393+ st .integers (min_value = 1 , max_value = size - 1 ),
394+ min_size = nchunks - 1 ,
395+ max_size = nchunks - 1 ,
396+ unique = True ,
397+ )
355398 )
356399 )
357- )
358- chunk_shapes .append (
359- [a - b for a , b in zip (dividers + [size ], [0 ] + dividers , strict = False )]
360- )
400+ chunk_shapes .append (
401+ [a - b for a , b in zip (dividers + [size ], [0 ] + dividers , strict = False )]
402+ )
403+ else :
404+ # RLE mode: uniform chunks with optional remainder
405+ max_chunk_size = min (size , 20 )
406+ chunk_size = draw (st .integers (min_value = 1 , max_value = max_chunk_size ))
407+ n_full = size // chunk_size
408+ remainder = size % chunk_size
409+ chunks_list = [chunk_size ] * n_full
410+ if remainder > 0 :
411+ chunks_list .append (remainder )
412+ # Optionally shuffle to create non-contiguous duplicate patterns
413+ if draw (st .booleans ()):
414+ event ("rectilinear rle shuffled" )
415+ chunks_list = draw (st .permutations (chunks_list ))
416+ else :
417+ event ("rectilinear rle" )
418+ chunk_shapes .append (list (chunks_list ))
419+ else :
420+ chunk_shapes .append ([1 ])
361421 return chunk_shapes
362422
363423
364- # Rectilinear arrays need min_side >= 2 so divider generation works
365- _rectilinear_shapes = npst .array_shapes (max_dims = 3 , min_side = 2 , max_side = 20 )
424+ @st .composite
425+ def chunk_grids (
426+ draw : st .DrawFn , * , shape : tuple [int , ...]
427+ ) -> RegularChunkGridMetadata | RectilinearChunkGridMetadata :
428+ """Generate either a RegularChunkGridMetadata or RectilinearChunkGridMetadata.
429+
430+ This allows property tests to exercise both chunk grid types.
431+ """
432+ # RectilinearChunkGridMetadata doesn't support zero-sized dimensions,
433+ # so use RegularChunkGridMetadata if any dimension is 0
434+ if any (s == 0 for s in shape ):
435+ event ("using RegularChunkGridMetadata (zero-sized dimensions)" )
436+ return RegularChunkGridMetadata (chunk_shape = draw (chunk_shapes (shape = shape )))
437+
438+ if draw (st .booleans ()):
439+ chunks = draw (rectilinear_chunks (shape = shape ))
440+ event ("using RectilinearChunkGridMetadata" )
441+ with zarr .config .set ({"array.rectilinear_chunks" : True }):
442+ return RectilinearChunkGridMetadata (chunk_shapes = tuple (tuple (dim ) for dim in chunks ))
443+ else :
444+ event ("using RegularChunkGridMetadata" )
445+ return RegularChunkGridMetadata (chunk_shape = draw (chunk_shapes (shape = shape )))
446+
447+
448+ # Rectilinear arrays need min_side >= 1 so every dimension has at least one element
449+ _rectilinear_shapes = npst .array_shapes (max_dims = 3 , min_side = 1 , max_side = 20 )
366450
367451
368452@st .composite
@@ -375,10 +459,21 @@ def rectilinear_arrays(
375459 shape = draw (shapes )
376460 chunk_shapes = draw (rectilinear_chunks (shape = shape ))
377461
378- nparray = np .arange (int (np .prod (shape )), dtype = "int32" ).reshape (shape )
462+ np_dtype = draw (dtypes ())
463+ nparray = draw (numpy_arrays (shapes = st .just (shape ), dtype = np_dtype ))
464+ fill_value = draw (st .one_of ([st .none (), npst .from_dtype (np_dtype )]))
465+ dim_names = draw (dimension_names (ndim = len (shape )))
466+
379467 store = MemoryStore ()
380468 with zarr .config .set ({"array.rectilinear_chunks" : True }):
381- a = zarr .create_array (store = store , shape = shape , chunks = chunk_shapes , dtype = "int32" )
469+ a = zarr .create_array (
470+ store = store ,
471+ shape = shape ,
472+ chunks = chunk_shapes ,
473+ dtype = np_dtype ,
474+ fill_value = fill_value ,
475+ dimension_names = dim_names ,
476+ )
382477 a [:] = nparray
383478
384479 return a
0 commit comments