@@ -107,7 +107,7 @@ def test_dimension_index_to_chunk_last_valid(
107107 lambda : RectilinearChunkGridMetadata .from_dict (
108108 {
109109 "name" : "rectilinear" ,
110- "configuration" : {"kind" : "inline" , "chunk_shapes" : [[10 , 20 , 30 ], [50 , 50 ]]}, # type: ignore[typeddict-item]
110+ "configuration" : {"kind" : "inline" , "chunk_shapes" : [[10 , 20 , 30 ], [50 , 50 ]]},
111111 }
112112 ),
113113 lambda : zarr .create_array (MemoryStore (), shape = (30 ,), chunks = [[10 , 20 ]], dtype = "int32" ),
@@ -546,13 +546,13 @@ def test_rle_expand_rejects_invalid(rle_input: list[Any], match: str) -> None:
546546
547547def test_expand_rle_bare_integer_floats_accepted () -> None :
548548 """JSON parsers may emit 10.0 for the integer 10; expand_rle should handle it."""
549- result = expand_rle ([10.0 , 20.0 ]) # type: ignore[list-item]
549+ result = expand_rle ([10.0 , 20.0 ])
550550 assert result == [10 , 20 ]
551551
552552
553553def test_expand_rle_pair_with_float_count () -> None :
554554 """expand_rle accepts float repeat counts that are integer-valued"""
555- result = expand_rle ([[10 , 3.0 ]]) # type: ignore[list-item]
555+ result = expand_rle ([[10 , 3.0 ]])
556556 assert result == [10 , 10 , 10 ]
557557
558558
@@ -595,6 +595,16 @@ def test_is_rectilinear_chunks(value: Any, expected: bool) -> None:
595595 assert _is_rectilinear_chunks (value ) is expected
596596
597597
598+ def test_is_rectilinear_chunks_handles_broken_iterable () -> None :
599+ """_is_rectilinear_chunks returns False for objects that raise on iteration."""
600+
601+ class BrokenIter :
602+ def __iter__ (self ) -> Any :
603+ raise TypeError ("cannot iterate" )
604+
605+ assert _is_rectilinear_chunks (BrokenIter ()) is False
606+
607+
598608# ---------------------------------------------------------------------------
599609# Serialization tests
600610# ---------------------------------------------------------------------------
@@ -619,6 +629,24 @@ def test_serialization_unknown_name_parse() -> None:
619629 parse_chunk_grid ({"name" : "hexagonal" , "configuration" : {}})
620630
621631
632+ def test_from_metadata_unknown_chunk_grid_type () -> None :
633+ """ChunkGrid.from_metadata raises TypeError for unrecognised chunk grid metadata."""
634+ from unittest .mock import MagicMock
635+
636+ from zarr .core .metadata .v3 import ArrayV3Metadata
637+
638+ mock_meta = MagicMock (spec = ArrayV3Metadata )
639+ mock_meta .chunk_grid = MagicMock () # not Regular or Rectilinear
640+ with pytest .raises (TypeError , match = "Unknown chunk grid metadata type" ):
641+ ChunkGrid .from_metadata (mock_meta )
642+
643+
644+ def test_from_sizes_rejects_empty_edge_list () -> None :
645+ """ChunkGrid.from_sizes raises ValueError when a dimension has an empty edge list."""
646+ with pytest .raises (ValueError , match = "at least one chunk" ):
647+ ChunkGrid .from_sizes ((10 ,), ([],))
648+
649+
622650# ---------------------------------------------------------------------------
623651# Spec compliance tests
624652# ---------------------------------------------------------------------------
@@ -651,7 +679,7 @@ def test_spec_integer_shorthand_per_dimension() -> None:
651679 "configuration" : {"kind" : "inline" , "chunk_shapes" : [4 , [1 , 2 , 3 ]]},
652680 }
653681 meta = parse_chunk_grid (data )
654- g = ChunkGrid .from_sizes ((6 , 6 ), meta .chunk_shapes ) # type: ignore[union-attr]
682+ g = ChunkGrid .from_sizes ((6 , 6 ), meta .chunk_shapes )
655683 assert _edges (g , 0 ) == (4 , 4 )
656684 assert _edges (g , 1 ) == (1 , 2 , 3 )
657685
@@ -663,7 +691,7 @@ def test_spec_mixed_rle_and_bare_integers() -> None:
663691 "configuration" : {"kind" : "inline" , "chunk_shapes" : [[[1 , 3 ], 3 ]]},
664692 }
665693 meta = parse_chunk_grid (data )
666- g = ChunkGrid .from_sizes ((6 ,), meta .chunk_shapes ) # type: ignore[union-attr]
694+ g = ChunkGrid .from_sizes ((6 ,), meta .chunk_shapes )
667695 assert _edges (g , 0 ) == (1 , 1 , 1 , 3 )
668696
669697
@@ -674,7 +702,7 @@ def test_spec_overflow_chunks_allowed() -> None:
674702 "configuration" : {"kind" : "inline" , "chunk_shapes" : [[4 , 4 , 4 ]]},
675703 }
676704 meta = parse_chunk_grid (data )
677- g = ChunkGrid .from_sizes ((6 ,), meta .chunk_shapes ) # type: ignore[union-attr]
705+ g = ChunkGrid .from_sizes ((6 ,), meta .chunk_shapes )
678706 assert _edges (g , 0 ) == (4 , 4 , 4 )
679707
680708
@@ -694,7 +722,7 @@ def test_spec_example() -> None:
694722 },
695723 }
696724 meta = parse_chunk_grid (data )
697- g = ChunkGrid .from_sizes ((6 , 6 , 6 , 6 , 6 ), meta .chunk_shapes ) # type: ignore[union-attr]
725+ g = ChunkGrid .from_sizes ((6 , 6 , 6 , 6 , 6 ), meta .chunk_shapes )
698726 assert _edges (g , 0 ) == (4 , 4 )
699727 assert _edges (g , 1 ) == (1 , 2 , 3 )
700728 assert _edges (g , 2 ) == (4 , 4 )
@@ -747,7 +775,7 @@ def test_parse_chunk_grid_rectilinear_extent_mismatch_raises(
747775 }
748776 meta = parse_chunk_grid (data )
749777 with pytest .raises (ValueError , match = match ):
750- ChunkGrid .from_sizes (array_shape , meta .chunk_shapes ) # type: ignore[union-attr]
778+ ChunkGrid .from_sizes (array_shape , meta .chunk_shapes )
751779
752780
753781def test_parse_chunk_grid_rectilinear_extent_match_passes () -> None :
@@ -757,7 +785,7 @@ def test_parse_chunk_grid_rectilinear_extent_match_passes() -> None:
757785 "configuration" : {"kind" : "inline" , "chunk_shapes" : [[10 , 20 , 30 ], [25 , 25 ]]},
758786 }
759787 meta = parse_chunk_grid (data )
760- g = ChunkGrid .from_sizes ((60 , 50 ), meta .chunk_shapes ) # type: ignore[union-attr]
788+ g = ChunkGrid .from_sizes ((60 , 50 ), meta .chunk_shapes )
761789 assert g .grid_shape == (3 , 2 )
762790
763791
@@ -769,7 +797,7 @@ def test_parse_chunk_grid_rectilinear_ndim_mismatch_raises() -> None:
769797 }
770798 meta = parse_chunk_grid (data )
771799 with pytest .raises (ValueError , match = "3 dimensions but chunk_sizes has 2" ):
772- ChunkGrid .from_sizes ((30 , 50 , 100 ), meta .chunk_shapes ) # type: ignore[union-attr]
800+ ChunkGrid .from_sizes ((30 , 50 , 100 ), meta .chunk_shapes )
773801
774802
775803def test_parse_chunk_grid_rectilinear_rle_extent_validated () -> None :
@@ -779,10 +807,10 @@ def test_parse_chunk_grid_rectilinear_rle_extent_validated() -> None:
779807 "configuration" : {"kind" : "inline" , "chunk_shapes" : [[[10 , 5 ]], [[25 , 2 ]]]},
780808 }
781809 meta = parse_chunk_grid (data )
782- g = ChunkGrid .from_sizes ((50 , 50 ), meta .chunk_shapes ) # type: ignore[union-attr]
810+ g = ChunkGrid .from_sizes ((50 , 50 ), meta .chunk_shapes )
783811 assert g .grid_shape == (5 , 2 )
784812 with pytest .raises (ValueError , match = "extent 100 exceeds sum of edges 50" ):
785- ChunkGrid .from_sizes ((100 , 50 ), meta .chunk_shapes ) # type: ignore[union-attr]
813+ ChunkGrid .from_sizes ((100 , 50 ), meta .chunk_shapes )
786814
787815
788816def test_parse_chunk_grid_varying_dimension_extent_mismatch_on_chunkgrid_input () -> None :
@@ -1044,6 +1072,31 @@ def test_sharding_accepts_divisible_rectilinear() -> None:
10441072 )
10451073
10461074
1075+ def test_sharding_rejects_non_divisible_among_repeated_edges () -> None :
1076+ """Shard validation catches a non-divisible edge even among many repeated valid ones."""
1077+ from zarr .codecs .sharding import ShardingCodec
1078+ from zarr .core .dtype import Float32
1079+ from zarr .core .metadata .v3 import RectilinearChunkGridMetadata
1080+
1081+ # edges (10, 10, 7) — 7 is not divisible by 5
1082+ codec = ShardingCodec (chunk_shape = (5 ,))
1083+ grid_meta = RectilinearChunkGridMetadata (chunk_shapes = ((10 , 10 , 7 ),))
1084+ with pytest .raises (ValueError , match = "divisible" ):
1085+ codec .validate (shape = (27 ,), dtype = Float32 (), chunk_grid = grid_meta )
1086+
1087+
1088+ def test_sharding_accepts_all_repeated_divisible_edges () -> None :
1089+ """Shard validation passes when all distinct edges are divisible by inner chunk size."""
1090+ from zarr .codecs .sharding import ShardingCodec
1091+ from zarr .core .dtype import Float32
1092+ from zarr .core .metadata .v3 import RectilinearChunkGridMetadata
1093+
1094+ # edges (10, 10, 20, 10) — unique values {10, 20}, both divisible by 5
1095+ codec = ShardingCodec (chunk_shape = (5 ,))
1096+ grid_meta = RectilinearChunkGridMetadata (chunk_shapes = ((10 , 10 , 20 , 10 ),))
1097+ codec .validate (shape = (50 ,), dtype = Float32 (), chunk_grid = grid_meta )
1098+
1099+
10471100# ---------------------------------------------------------------------------
10481101# Edge cases
10491102# ---------------------------------------------------------------------------
@@ -1093,6 +1146,35 @@ def test_edge_case_zero_size_or_extent(size: int, extent: int) -> None:
10931146 assert g [0 ] is None
10941147
10951148
1149+ def test_edge_case_zero_size_data_and_indices () -> None :
1150+ """FixedDimension(size=0) handles data_size, index_to_chunk, and indices_to_chunks safely."""
1151+ d = FixedDimension (size = 0 , extent = 0 )
1152+ # Zero-sized chunks have zero data
1153+ assert d .data_size (0 ) == 0
1154+ # Vectorized lookup maps every index to chunk 0 (avoids division by zero)
1155+ indices = np .array ([0 , 0 , 0 ], dtype = np .intp )
1156+ np .testing .assert_array_equal (d .indices_to_chunks (indices ), np .zeros (3 , dtype = np .intp ))
1157+
1158+
1159+ def test_edge_case_zero_size_nonzero_extent_index () -> None :
1160+ """FixedDimension(size=0, extent>0) maps valid indices to chunk 0 without dividing by zero."""
1161+ d = FixedDimension (size = 0 , extent = 5 )
1162+ assert d .nchunks == 0
1163+ # index_to_chunk avoids division by zero and returns 0
1164+ assert d .index_to_chunk (0 ) == 0
1165+ assert d .index_to_chunk (4 ) == 0
1166+
1167+
1168+ def test_edge_case_zero_size_data_and_index () -> None :
1169+ """FixedDimension(size=0) returns zero for data_size and maps indices to chunk 0."""
1170+ d = FixedDimension (size = 0 , extent = 0 )
1171+ # data_size returns 0 for a zero-sized chunk
1172+ assert d .data_size (0 ) == 0
1173+ # vectorized indices_to_chunks returns zeros
1174+ indices = np .array ([0 , 0 , 0 ], dtype = np .intp )
1175+ np .testing .assert_array_equal (d .indices_to_chunks (indices ), np .zeros (3 , dtype = np .intp ))
1176+
1177+
10961178# -- 0-d grid --
10971179
10981180
@@ -1753,6 +1835,17 @@ def test_varying_dimension_negative_extent_rejected() -> None:
17531835 VaryingDimension ([10 , 20 ], extent = - 1 )
17541836
17551837
1838+ def test_varying_dimension_zero_extent () -> None :
1839+ """VaryingDimension with extent=0 has zero active chunks but retains all grid cells."""
1840+ d = VaryingDimension ([10 , 20 ], extent = 0 )
1841+ assert d .nchunks == 0
1842+ assert d .ngridcells == 2
1843+ # No chunks overlap [0, 0), so the grid is structurally non-empty but logically empty
1844+ g = ChunkGrid (dimensions = (d ,))
1845+ assert g .grid_shape == (0 ,)
1846+ assert list (g ) == []
1847+
1848+
17561849def test_varying_dimension_boundary_chunk_spec () -> None :
17571850 """ChunkGrid with a boundary VaryingDimension produces correct ChunkSpec."""
17581851 g = ChunkGrid (dimensions = (VaryingDimension ([10 , 20 , 30 ], extent = 50 ),))
@@ -2508,6 +2601,21 @@ def test_array_sharded_chunk_sizes() -> None:
25082601# ---------------------------------------------------------------------------
25092602
25102603
2604+ def test_chunk_grid_repr_regular () -> None :
2605+ """ChunkGrid repr shows uniform chunk sizes and array shape for regular grids."""
2606+ grid = ChunkGrid .from_sizes ((100 , 200 ), (10 , 20 ))
2607+ r = repr (grid )
2608+ assert r == "ChunkGrid(chunk_sizes=(10, 20), array_shape=(100, 200))"
2609+
2610+
2611+ def test_chunk_grid_repr_rectilinear () -> None :
2612+ """ChunkGrid repr shows per-chunk edge tuples for rectilinear dimensions."""
2613+ grid = ChunkGrid .from_sizes ((30 ,), ([10 , 20 ],))
2614+ r = repr (grid )
2615+ assert "(10, 20)" in r
2616+ assert "(30,)" in r
2617+
2618+
25112619def test_info_display_rectilinear () -> None :
25122620 """Array.info should not crash for rectilinear grids."""
25132621 store = zarr .storage .MemoryStore ()
@@ -2612,7 +2720,7 @@ def test_rectilinear_from_dict(
26122720 json_input : dict [str , Any ], expected_chunk_shapes : tuple [int | tuple [int , ...], ...]
26132721) -> None :
26142722 """RectilinearChunkGridMetadata.from_dict correctly parses all spec forms."""
2615- grid = RectilinearChunkGridMetadata .from_dict (json_input ) # type: ignore[arg-type]
2723+ grid = RectilinearChunkGridMetadata .from_dict (json_input )
26162724 assert grid .chunk_shapes == expected_chunk_shapes
26172725
26182726
@@ -2654,7 +2762,7 @@ def test_rectilinear_to_dict(
26542762)
26552763def test_rectilinear_roundtrip (json_input : dict [str , Any ]) -> None :
26562764 """from_dict -> to_dict -> from_dict produces the same grid."""
2657- grid1 = RectilinearChunkGridMetadata .from_dict (json_input ) # type: ignore[arg-type]
2765+ grid1 = RectilinearChunkGridMetadata .from_dict (json_input )
26582766 grid2 = RectilinearChunkGridMetadata .from_dict (grid1 .to_dict ())
26592767 assert grid1 .chunk_shapes == grid2 .chunk_shapes
26602768
0 commit comments