1+ import math
12import sys
23from typing import Any , Literal
34
45import hypothesis .extra .numpy as npst
56import hypothesis .strategies as st
67import numpy as np
7- from hypothesis import given , settings # noqa: F401
8+ from hypothesis import event , given , settings # noqa: F401
89from hypothesis .strategies import SearchStrategy
910
1011import zarr
2829)
2930
3031
32+ @st .composite # type: ignore[misc]
33+ def keys (draw : st .DrawFn , * , max_num_nodes : int | None = None ) -> Any :
34+ return draw (st .lists (node_names , min_size = 1 , max_size = max_num_nodes ).map ("/" .join ))
35+
36+
37+ @st .composite # type: ignore[misc]
38+ def paths (draw : st .DrawFn , * , max_num_nodes : int | None = None ) -> Any :
39+ return draw (st .just ("/" ) | keys (max_num_nodes = max_num_nodes ))
40+
41+
3142def v3_dtypes () -> st .SearchStrategy [np .dtype ]:
3243 return (
3344 npst .boolean_dtypes ()
@@ -87,17 +98,19 @@ def clear_store(x: Store) -> Store:
8798node_names = st .text (zarr_key_chars , min_size = 1 ).filter (
8899 lambda t : t not in ("." , ".." ) and not t .startswith ("__" )
89100)
101+ short_node_names = st .text (zarr_key_chars , max_size = 3 , min_size = 1 ).filter (
102+ lambda t : t not in ("." , ".." ) and not t .startswith ("__" )
103+ )
90104array_names = node_names
91105attrs = st .none () | st .dictionaries (_attr_keys , _attr_values )
92- keys = st .lists (node_names , min_size = 1 ).map ("/" .join )
93- paths = st .just ("/" ) | keys
94106# st.builds will only call a new store constructor for different keyword arguments
95107# i.e. stores.examples() will always return the same object per Store class.
96108# So we map a clear to reset the store.
97109stores = st .builds (MemoryStore , st .just ({})).map (clear_store )
98110compressors = st .sampled_from ([None , "default" ])
99111zarr_formats : st .SearchStrategy [ZarrFormat ] = st .sampled_from ([3 , 2 ])
100- array_shapes = npst .array_shapes (max_dims = 4 , min_side = 0 )
112+ # We de-prioritize arrays having dim sizes 0, 1, 2
113+ array_shapes = npst .array_shapes (max_dims = 4 , min_side = 3 ) | npst .array_shapes (max_dims = 4 , min_side = 0 )
101114
102115
103116@st .composite # type: ignore[misc]
@@ -152,13 +165,15 @@ def numpy_arrays(
152165 draw : st .DrawFn ,
153166 * ,
154167 shapes : st .SearchStrategy [tuple [int , ...]] = array_shapes ,
155- zarr_formats : st .SearchStrategy [ZarrFormat ] = zarr_formats ,
168+ dtype : np .dtype [Any ] | None = None ,
169+ zarr_formats : st .SearchStrategy [ZarrFormat ] | None = zarr_formats ,
156170) -> Any :
157171 """
158172 Generate numpy arrays that can be saved in the provided Zarr format.
159173 """
160174 zarr_format = draw (zarr_formats )
161- dtype = draw (v3_dtypes () if zarr_format == 3 else v2_dtypes ())
175+ if dtype is None :
176+ dtype = draw (v3_dtypes () if zarr_format == 3 else v2_dtypes ())
162177 if np .issubdtype (dtype , np .str_ ):
163178 safe_unicode_strings = safe_unicode_for_dtype (dtype )
164179 return draw (npst .arrays (dtype = dtype , shape = shapes , elements = safe_unicode_strings ))
@@ -174,11 +189,19 @@ def chunk_shapes(draw: st.DrawFn, *, shape: tuple[int, ...]) -> tuple[int, ...]:
174189 st .tuples (* [st .integers (min_value = 0 if size == 0 else 1 , max_value = size ) for size in shape ])
175190 )
176191 # 2. and now generate the chunks tuple
177- return tuple (
192+ chunks = tuple (
178193 size // nchunks if nchunks > 0 else 0
179194 for size , nchunks in zip (shape , numchunks , strict = True )
180195 )
181196
197+ for c in chunks :
198+ event ("chunk size" , c )
199+
200+ if any ((c != 0 and s % c != 0 ) for s , c in zip (shape , chunks , strict = True )):
201+ event ("smaller last chunk" )
202+
203+ return chunks
204+
182205
183206@st .composite # type: ignore[misc]
184207def shard_shapes (
@@ -211,7 +234,7 @@ def arrays(
211234 shapes : st .SearchStrategy [tuple [int , ...]] = array_shapes ,
212235 compressors : st .SearchStrategy = compressors ,
213236 stores : st .SearchStrategy [StoreLike ] = stores ,
214- paths : st .SearchStrategy [str | None ] = paths ,
237+ paths : st .SearchStrategy [str | None ] = paths (), # noqa: B008
215238 array_names : st .SearchStrategy = array_names ,
216239 arrays : st .SearchStrategy | None = None ,
217240 attrs : st .SearchStrategy = attrs ,
@@ -267,23 +290,56 @@ def arrays(
267290 return a
268291
269292
293+ @st .composite # type: ignore[misc]
294+ def simple_arrays (
295+ draw : st .DrawFn ,
296+ * ,
297+ shapes : st .SearchStrategy [tuple [int , ...]] = array_shapes ,
298+ ) -> Any :
299+ return draw (
300+ arrays (
301+ shapes = shapes ,
302+ paths = paths (max_num_nodes = 2 ),
303+ array_names = short_node_names ,
304+ attrs = st .none (),
305+ compressors = st .sampled_from ([None , "default" ]),
306+ )
307+ )
308+
309+
270310def is_negative_slice (idx : Any ) -> bool :
271311 return isinstance (idx , slice ) and idx .step is not None and idx .step < 0
272312
273313
314+ @st .composite # type: ignore[misc]
315+ def end_slices (draw : st .DrawFn , * , shape : tuple [int ]) -> Any :
316+ """
317+ A strategy that slices ranges that include the last chunk.
318+ This is intended to stress-test handling of a possibly smaller last chunk.
319+ """
320+ slicers = []
321+ for size in shape :
322+ start = draw (st .integers (min_value = size // 2 , max_value = size - 1 ))
323+ length = draw (st .integers (min_value = 0 , max_value = size - start ))
324+ slicers .append (slice (start , start + length ))
325+ event ("drawing end slice" )
326+ return tuple (slicers )
327+
328+
274329@st .composite # type: ignore[misc]
275330def basic_indices (draw : st .DrawFn , * , shape : tuple [int ], ** kwargs : Any ) -> Any :
276331 """Basic indices without unsupported negative slices."""
277- return draw (
278- npst .basic_indices (shape = shape , ** kwargs ).filter (
279- lambda idxr : (
280- not (
281- is_negative_slice (idxr )
282- or (isinstance (idxr , tuple ) and any (is_negative_slice (idx ) for idx in idxr ))
283- )
332+ strategy = npst .basic_indices (shape = shape , ** kwargs ).filter (
333+ lambda idxr : (
334+ not (
335+ is_negative_slice (idxr )
336+ or (isinstance (idxr , tuple ) and any (is_negative_slice (idx ) for idx in idxr ))
284337 )
285338 )
286339 )
340+ if math .prod (shape ) >= 3 :
341+ strategy = end_slices (shape = shape ) | strategy
342+ return draw (strategy )
287343
288344
289345@st .composite # type: ignore[misc]
0 commit comments