Skip to content

Commit 3e1a62d

Browse files
authored
Merge branch 'main' into docs/from-zero-to-zarr
2 parents afd08ed + 122d974 commit 3e1a62d

20 files changed

Lines changed: 858 additions & 573 deletions

.github/dependabot.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ updates:
1212
- "*"
1313
cooldown:
1414
default-days: 7
15+
# Keep the pinned dev tooling in pyproject.toml's [dependency-groups] and the
16+
# uv.lock current. Without this the exact pins (e.g. pytest) would never be
17+
# bumped automatically and would silently rot.
18+
- package-ecosystem: "uv"
19+
directory: "/"
20+
schedule:
21+
interval: "weekly"
22+
groups:
23+
python-dependencies:
24+
patterns:
25+
- "*"
26+
cooldown:
27+
default-days: 7
1528
- package-ecosystem: "github-actions"
1629
directory: "/"
1730
target-branch: "support/v2"

.github/workflows/gpu_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
hatch env run --env "$HATCH_ENV" run-coverage
8181
8282
- name: Upload coverage
83-
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
83+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
8484
with:
8585
token: ${{ secrets.CODECOV_TOKEN }}
8686
flags: gpu

.github/workflows/hypothesis.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
key: cache-hypothesis-${{ runner.os }}-${{ github.run_id }}
9898

9999
- name: Upload coverage
100-
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
100+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
101101
with:
102102
token: ${{ secrets.CODECOV_TOKEN }}
103103
flags: tests

.github/workflows/prepare_release.yml

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,12 @@ jobs:
4242
fetch-depth: 0
4343
persist-credentials: false
4444

45-
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
46-
with:
47-
python-version: '3.12'
48-
49-
- name: Install towncrier
50-
run: pip install towncrier
45+
- uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
5146

5247
- name: Build changelog
53-
run: towncrier build --version "$VERSION" --yes
48+
# Use the pinned towncrier from the `release` dependency group (single
49+
# source of truth) rather than an unpinned standalone install.
50+
run: uv run --only-group release towncrier build --version "$VERSION" --yes
5451
env:
5552
VERSION: ${{ inputs.version }}
5653

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
hatch env run --env "$HATCH_ENV" run-coverage
8484
- name: Upload coverage
8585
if: ${{ matrix.dependency-set == 'optional' && matrix.os == 'ubuntu-latest' }}
86-
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
86+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
8787
with:
8888
token: ${{ secrets.CODECOV_TOKEN }}
8989
flags: tests
@@ -130,7 +130,7 @@ jobs:
130130
run: |
131131
hatch env run --env "$HATCH_ENV" run-coverage
132132
- name: Upload coverage
133-
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
133+
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
134134
with:
135135
token: ${{ secrets.CODECOV_TOKEN }}
136136
flags: tests

changes/4074.bugfix.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed several storage and codec bugs:
2+
3+
- Reading a value with a `SuffixByteRequest` larger than the value now correctly returns the whole value (matching HTTP `bytes=-N` suffix-range semantics), instead of silently returning incorrect data for `MemoryStore`.
4+
- `LoggingStore.get_partial_values` and `FsspecStore.get_partial_values` no longer return empty results when `key_ranges` is passed as a one-shot iterable (e.g. a generator).
5+
- `Store.getsize_prefix` no longer over-counts sibling keys that merely share a string prefix (e.g. `getsize_prefix("foo")` no longer includes keys under `foobar/`).
6+
- `ZipStore.close()` no longer raises `AttributeError` when the store was created but never opened (including when used as a context manager without any I/O).
7+
- `codecs_from_list` now raises a descriptive `TypeError` when a `BytesBytesCodec` immediately follows an `ArrayArrayCodec`, instead of a misleading "Required ArrayBytesCodec was not found" `ValueError`.

pyproject.toml

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -83,56 +83,68 @@ Discussions = "https://github.com/zarr-developers/zarr-python/discussions"
8383
documentation = "https://zarr.readthedocs.io/"
8484
homepage = "https://github.com/zarr-developers/zarr-python"
8585

86+
# Dev *tooling* is pinned to exact versions for reproducible CI: the hatch envs
87+
# (see `tool.hatch.envs.*`) and bare `uv run` resolve these groups fresh from
88+
# PyPI and do NOT consult uv.lock, so an unrelated tooling release can break CI
89+
# without any change on our side (e.g. the pytest 9.1.0 `duplicate
90+
# parametrization` regression). Runtime/integration deps (fsspec, obstore, s3fs,
91+
# botocore, numcodecs, universal-pathlib) are intentionally left floating so the
92+
# `optional` test matrix keeps exercising their latest releases; their floor and
93+
# bleeding edge are covered by the `min_deps` and `upstream` hatch envs. Bump the
94+
# pins deliberately, e.g. via dependabot or `uv lock --upgrade`.
8695
[dependency-groups]
8796
test = [
88-
"coverage>=7.10",
89-
"pytest",
90-
"pytest-asyncio",
91-
"pytest-cov",
92-
"pytest-accept",
93-
"numpydoc",
94-
"hypothesis",
95-
"pytest-xdist",
96-
"pytest-benchmark",
97-
"pytest-codspeed",
98-
"tomlkit",
99-
"uv",
97+
"coverage==7.14.1",
98+
"pytest==9.0.3",
99+
"pytest-asyncio==1.4.0",
100+
"pytest-cov==7.1.0",
101+
"pytest-accept==0.2.3",
102+
"numpydoc==1.10.0",
103+
"hypothesis==6.155.2",
104+
"pytest-xdist==3.8.0",
105+
"pytest-benchmark==5.2.3",
106+
"pytest-codspeed==5.0.3",
107+
"tomlkit==0.15.0",
108+
"uv==0.11.19",
100109
]
101110
remote-tests = [
102111
{include-group = "test"},
103112
"fsspec>=2023.10.0",
104113
"obstore>=0.5.1",
105114
"botocore",
106115
"s3fs>=2023.10.0",
107-
"moto[s3,server]",
108-
"requests",
116+
"moto[s3,server]==5.2.2",
117+
"requests==2.34.2",
118+
]
119+
release = [
120+
"towncrier==25.8.0",
109121
]
110122
docs = [
111123
# Doc building
112-
"mkdocs-material[imaging]>=9.6.14",
113-
"mkdocs>=1.6.1,<2",
114-
"mkdocstrings>=0.29.1",
115-
"mkdocstrings-python>=1.16.10",
116-
"mike>=2.1.3",
117-
"mkdocs-jupyter>=0.25.1",
118-
"mkdocs-redirects>=1.2.0",
119-
"markdown-exec[ansi]",
120-
"griffe-inherited-docstrings",
121-
"ruff",
124+
"mkdocs-material[imaging]==9.7.6",
125+
"mkdocs==1.6.1",
126+
"mkdocstrings==1.0.4",
127+
"mkdocstrings-python==2.0.4",
128+
"mike==2.2.0",
129+
"mkdocs-jupyter==0.26.3",
130+
"mkdocs-redirects==1.2.3",
131+
"markdown-exec[ansi]==1.12.1",
132+
"griffe-inherited-docstrings==1.1.3",
133+
"ruff==0.15.16",
122134
# Changelog generation
123-
"towncrier",
135+
{include-group = "release"},
124136
# Optional dependencies to run examples
125137
"numcodecs[msgpack]",
126138
"s3fs>=2023.10.0",
127-
"astroid<4",
128-
"pytest",
139+
"astroid==4.1.2",
140+
"pytest==9.0.3",
129141
]
130142
dev = [
131143
{include-group = "test"},
132144
{include-group = "remote-tests"},
133145
{include-group = "docs"},
134146
"universal-pathlib",
135-
"mypy",
147+
"mypy==2.1.0",
136148
]
137149

138150
[tool.coverage.report]
@@ -381,7 +393,7 @@ show_error_code_links = true
381393
show_error_context = true
382394
strict = true
383395
warn_unreachable = true
384-
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
396+
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool", "truthy-iterable"]
385397

386398
[[tool.mypy.overrides]]
387399
module = [

src/zarr/abc/store.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ async def getsize_prefix(self, prefix: str) -> int:
536536
from zarr.core.common import concurrent_map
537537
from zarr.core.config import config
538538

539+
if prefix != "" and not prefix.endswith("/"):
540+
prefix += "/"
539541
keys = [(x,) async for x in self.list_prefix(prefix)]
540542
limit = config.get("async.concurrency")
541543
sizes = await concurrent_map(keys, self.getsize, limit=limit)

src/zarr/core/codec_pipeline.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,7 @@ def codecs_from_list(
679679
"must be preceded by either another BytesBytesCodec, or an ArrayBytesCodec. "
680680
f"Got {type(prev_codec)} instead."
681681
)
682+
raise TypeError(msg)
682683
bytes_bytes += (cur_codec,)
683684
else:
684685
raise TypeError

src/zarr/storage/_fsspec.py

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -424,30 +424,31 @@ async def get_partial_values(
424424
key_ranges: Iterable[tuple[str, ByteRequest | None]],
425425
) -> list[Buffer | None]:
426426
# docstring inherited
427-
if key_ranges:
428-
# _cat_ranges expects a list of paths, start, and end ranges, so we need to reformat each ByteRequest.
429-
key_ranges = list(key_ranges)
430-
paths: list[str] = []
431-
starts: list[int | None] = []
432-
stops: list[int | None] = []
433-
for key, byte_range in key_ranges:
434-
paths.append(_dereference_path(self.path, key))
435-
if byte_range is None:
436-
starts.append(None)
437-
stops.append(None)
438-
elif isinstance(byte_range, RangeByteRequest):
439-
starts.append(byte_range.start)
440-
stops.append(byte_range.end)
441-
elif isinstance(byte_range, OffsetByteRequest):
442-
starts.append(byte_range.offset)
443-
stops.append(None)
444-
elif isinstance(byte_range, SuffixByteRequest):
445-
starts.append(-byte_range.suffix)
446-
stops.append(None)
447-
else:
448-
raise ValueError(f"Unexpected byte_range, got {byte_range}.")
449-
else:
427+
# Materialise first: key_ranges may be a one-shot iterable, so a bare
428+
# truthiness check (e.g. `if key_ranges`) would be unreliable for an
429+
# empty generator. _cat_ranges also expects lists of paths/starts/stops.
430+
key_ranges = list(key_ranges)
431+
if not key_ranges:
450432
return []
433+
paths: list[str] = []
434+
starts: list[int | None] = []
435+
stops: list[int | None] = []
436+
for key, byte_range in key_ranges:
437+
paths.append(_dereference_path(self.path, key))
438+
if byte_range is None:
439+
starts.append(None)
440+
stops.append(None)
441+
elif isinstance(byte_range, RangeByteRequest):
442+
starts.append(byte_range.start)
443+
stops.append(byte_range.end)
444+
elif isinstance(byte_range, OffsetByteRequest):
445+
starts.append(byte_range.offset)
446+
stops.append(None)
447+
elif isinstance(byte_range, SuffixByteRequest):
448+
starts.append(-byte_range.suffix)
449+
stops.append(None)
450+
else:
451+
raise ValueError(f"Unexpected byte_range, got {byte_range}.")
451452
# TODO: expectations for exceptions or missing keys?
452453
res = await self.fs._cat_ranges(paths, starts, stops, on_error="return")
453454
# the following is an s3-specific condition we probably don't want to leak

0 commit comments

Comments
 (0)