Skip to content

Commit a66b208

Browse files
authored
Merge branch 'main' into dependabot/github_actions/actions-31ef6db2d7
2 parents baffb68 + 57e66d9 commit a66b208

6 files changed

Lines changed: 444 additions & 328 deletions

File tree

changes/4219.bugfix.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
`DefaultChunkKeyEncoding.decode_chunk_key` now validates that a chunk key
2+
starts with the configured `c<separator>` prefix and raises `ValueError` for
3+
malformed keys, instead of silently decoding them incorrectly.

pyproject.toml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,18 +94,18 @@ homepage = "https://github.com/zarr-developers/zarr-python"
9494
# pins deliberately, e.g. via dependabot or `uv lock --upgrade`.
9595
[dependency-groups]
9696
test = [
97-
"coverage==7.14.3",
97+
"coverage==7.15.2",
9898
"pytest==9.1.1",
9999
"pytest-asyncio==1.4.0",
100100
"pytest-cov==7.1.0",
101101
"pytest-accept==0.3.0",
102102
"numpydoc==1.10.0",
103-
"hypothesis==6.155.7",
103+
"hypothesis==6.160.0",
104104
"pytest-xdist==3.8.0",
105105
"pytest-benchmark==5.2.3",
106106
"pytest-codspeed==5.0.3",
107-
"tomlkit==0.15.0",
108-
"uv==0.11.26",
107+
"tomlkit==0.15.1",
108+
"uv==0.11.31",
109109
]
110110
remote-tests = [
111111
{include-group = "test"},
@@ -121,15 +121,15 @@ release = [
121121
]
122122
docs = [
123123
# Doc building
124-
"mkdocs-material[imaging]==9.7.6",
124+
"mkdocs-material[imaging]==9.7.7",
125125
"mkdocs==1.6.1",
126-
"mkdocstrings==1.0.4",
126+
"mkdocstrings==1.0.6",
127127
"mkdocstrings-python==2.0.5",
128128
"mike==2.2.0",
129129
"mkdocs-redirects==1.2.3",
130-
"markdown-exec[ansi]==1.12.1",
130+
"markdown-exec[ansi]==1.12.3",
131131
"griffe-inherited-docstrings==1.1.3",
132-
"ruff==0.15.20",
132+
"ruff==0.15.22",
133133
# Changelog generation
134134
{include-group = "release"},
135135
# Optional dependencies to run examples
@@ -143,7 +143,7 @@ dev = [
143143
{include-group = "remote-tests"},
144144
{include-group = "docs"},
145145
"universal-pathlib",
146-
"mypy==2.1.0",
146+
"mypy==2.3.0",
147147
]
148148

149149
[tool.coverage.report]

src/zarr/core/chunk_key_encodings.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ def __post_init__(self) -> None:
7979
def decode_chunk_key(self, chunk_key: str) -> tuple[int, ...]:
8080
if chunk_key == "c":
8181
return ()
82-
return tuple(map(int, chunk_key[1:].split(self.separator)))
82+
# Strip the "c<sep>" prefix (e.g. "c/" or "c.") before splitting.
83+
prefix = "c" + self.separator
84+
if chunk_key.startswith(prefix):
85+
return tuple(map(int, chunk_key[len(prefix) :].split(self.separator)))
86+
raise ValueError(f"Invalid chunk key for default encoding: {chunk_key!r}")
8387

8488
def encode_chunk_key(self, chunk_coords: tuple[int, ...]) -> str:
8589
return self.separator.join(map(str, ("c",) + chunk_coords))

src/zarr/core/indexing.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ def replace_ellipsis(selection: Any, shape: tuple[int, ...]) -> SelectionNormali
512512

513513
def replace_lists(selection: SelectionNormalized) -> SelectionNormalized:
514514
return tuple(
515-
np.asarray(dim_sel) if isinstance(dim_sel, list) else dim_sel for dim_sel in selection
515+
cast("ArrayOfIntOrBool", np.asarray(dim_sel)) if isinstance(dim_sel, list) else dim_sel
516+
for dim_sel in selection
516517
)
517518

518519

@@ -1193,7 +1194,7 @@ def __init__(
11931194
# some initial normalization
11941195
selection_normalized = cast("CoordinateSelectionNormalized", ensure_tuple(selection))
11951196
selection_normalized = tuple(
1196-
np.asarray([i]) if is_integer(i) else i for i in selection_normalized
1197+
np.asarray([i], dtype=np.intp) if is_integer(i) else i for i in selection_normalized
11971198
)
11981199
selection_normalized = cast(
11991200
"CoordinateSelectionNormalized", replace_lists(selection_normalized)

tests/test_chunk_key_encodings.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
from zarr.core.chunk_key_encodings import DefaultChunkKeyEncoding, V2ChunkKeyEncoding
6+
7+
8+
@pytest.mark.parametrize("separator", ["/", "."])
9+
@pytest.mark.parametrize(
10+
"coords",
11+
[(), (0,), (1, 2), (10, 0, 3)],
12+
)
13+
def test_default_encoding_round_trips(separator: str, coords: tuple[int, ...]) -> None:
14+
"""Encoding coordinates and decoding the result returns the coordinates."""
15+
encoding = DefaultChunkKeyEncoding(separator=separator) # type: ignore[arg-type]
16+
17+
key = encoding.encode_chunk_key(coords)
18+
assert encoding.decode_chunk_key(key) == coords
19+
20+
21+
@pytest.mark.parametrize("separator", ["/", "."])
22+
@pytest.mark.parametrize("coords", [(0,), (1, 2), (10, 0, 3)])
23+
def test_v2_encoding_round_trips(separator: str, coords: tuple[int, ...]) -> None:
24+
"""The v2 encoding round-trips coordinates for either separator."""
25+
encoding = V2ChunkKeyEncoding(separator=separator) # type: ignore[arg-type]
26+
27+
key = encoding.encode_chunk_key(coords)
28+
assert encoding.decode_chunk_key(key) == coords
29+
30+
31+
@pytest.mark.parametrize("separator", ["/", "."])
32+
def test_v2_zero_dimensional_key_is_ambiguous(separator: str) -> None:
33+
"""A 0-d v2 array stores its sole chunk under `"0"`, the same key a 1-d
34+
array uses for chunk 0, so decoding cannot recover the empty tuple on its
35+
own -- the array's dimensionality is what disambiguates it."""
36+
encoding = V2ChunkKeyEncoding(separator=separator) # type: ignore[arg-type]
37+
38+
assert encoding.encode_chunk_key(()) == "0"
39+
assert encoding.decode_chunk_key("0") == (0,)
40+
41+
42+
@pytest.mark.parametrize(
43+
"chunk_key",
44+
[
45+
"0/1", # no "c" prefix at all
46+
"c0/1", # "c" not followed by the separator
47+
"x/0/1", # wrong prefix character
48+
"",
49+
],
50+
)
51+
def test_default_encoding_rejects_key_without_prefix(chunk_key: str) -> None:
52+
"""A key that does not carry the `c<separator>` prefix is not a chunk key
53+
for this encoding, and must be rejected rather than silently decoded."""
54+
encoding = DefaultChunkKeyEncoding(separator="/")
55+
56+
with pytest.raises(ValueError, match="Invalid chunk key"):
57+
encoding.decode_chunk_key(chunk_key)
58+
59+
60+
def test_default_encoding_rejects_key_using_the_other_separator() -> None:
61+
"""A key encoded with `.` is not valid for a `/`-separated encoding."""
62+
encoding = DefaultChunkKeyEncoding(separator="/")
63+
64+
with pytest.raises(ValueError, match="Invalid chunk key"):
65+
encoding.decode_chunk_key("c.0.1")

0 commit comments

Comments
 (0)