Skip to content

fix: resolve chunks=False to chunk size 1 on zero-length axes - #4328

Merged
d-v-b merged 3 commits into
zarr-developers:mainfrom
d-v-b:fix/chunks-false-zero-length-axis
Sep 9, 2026
Merged

fix: resolve chunks=False to chunk size 1 on zero-length axes#4328
d-v-b merged 3 commits into
zarr-developers:mainfrom
d-v-b:fix/chunks-false-zero-length-axis

Conversation

@d-v-b

@d-v-b d-v-b commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

This AI-authored PR ensures that chunks=False (a pattern I dislike very much, but which we keep around for backwards compatibility), doesn't create 0-length chunks when the array shape is 0.

🤖 AI text below 🤖

Summary

chunks=False means "one chunk covering the whole array", but on a zero-length axis it produced a chunk of size 0. For Zarr format 3 this was rejected by RegularChunkGridMetadata (integer chunk edge length must be >= 1, got 0); with shards="auto" it raised ZeroDivisionError; and for Zarr format 2 it silently wrote chunks: [0], which corrupted reads once the axis was appended to and resized — the degenerate grid reports zero chunks, so reads return uninitialized memory or zeros.

#4307 already fixed the equivalent chunks=-1 sentinel by clamping to max(span, 1), matching chunks="auto". This PR routes the False branch of normalize_chunks_nd through that same -1 path so there is a single clamp; both spellings now yield chunk size 1 on an empty axis.

The #4307 regression test is extended to cover False alongside -1 for (0,), (0, 4) and (4, 0) on both formats, with and without shards="auto" (and with a shard-size budget), asserting the in-memory chunks/shards, the stored chunk grid, and that append/resize along the empty axis round-trip data. Without the source change exactly the 14 False parametrizations fail and every -1 case passes.

Found during the pre-release review for #4256.

For reviewers

The source change is a two-line fallthrough (chunks = -1) replacing the direct FixedDimension construction in the False branch, on top of #4218's chunk-grid rewrite. The v2 behavior change is intentional: the previous chunks: [0] metadata was invalid and returned wrong data after a resize, and no existing test depended on it.

Author attestation

  • I am a human, these are my changes, and I have reviewed and understood every change and can explain why each is correct.

TODO

  • Add unit tests and/or doctests in docstrings
  • Add docstrings and API docs for any new/modified user-facing classes and functions (n/a)
  • New/modified features documented in docs/user-guide/*.md (n/a)
  • Changes documented as a new file in changes/
  • GitHub Actions have all passed
  • Test coverage is 100% (Codecov passes)

`chunks=False` means "one chunk covering the whole array", but on a
zero-length axis it built a chunk of size 0. For Zarr format 3 this was
rejected by RegularChunkGridMetadata with "integer chunk edge length
must be >= 1, got 0"; with shards="auto" it raised ZeroDivisionError on
both formats; and for Zarr format 2 it silently wrote `chunks: [0]`,
which corrupted reads after the axis was later resized.

Route `False` through the `-1` sentinel so the zero-length clamp added
in zarr-developers#4307 (max(span, 1), matching _guess_regular_chunks) lives in one
place. Both spellings now yield chunk size 1 on empty axes.

Rebased over zarr-developers#4218, which rewrote normalize_chunks_nd to build
FixedDimension/VaryingDimension grids: the False branch now falls
through to the -1 path instead of returning a ChunkGrid directly, and
the test table expectations use the new bare-int / tuple form.

Assisted-by: ClaudeCode:claude-fable-5-1
Assisted-by: ClaudeCode:claude-fable-5-1
@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.31%. Comparing base (a1b4416) to head (2cc2126).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4328   +/-   ##
=======================================
  Coverage   94.31%   94.31%           
=======================================
  Files          92       92           
  Lines       12920    12920           
=======================================
  Hits        12185    12185           
  Misses        735      735           
Files with missing lines Coverage Δ
src/zarr/core/chunk_grids.py 96.73% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@d-v-b
d-v-b marked this pull request as ready for review September 9, 2026 17:42
@d-v-b

d-v-b commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

this is a simple bugfix, so i'm self-merging

@d-v-b
d-v-b merged commit 10f8abe into zarr-developers:main Sep 9, 2026
39 checks passed
@d-v-b
d-v-b deleted the fix/chunks-false-zero-length-axis branch September 9, 2026 18:03
d-v-b added a commit to d-v-b/zarr-python that referenced this pull request Sep 9, 2026
…, clamps and metadata

Invariant: a chunk edge length is always >= 1; a dimension's extent may be 0,
in which case the dimension has zero chunks (ceildiv(0, size) == 0).

Zero-length-axis bugs have recurred since 2017 (#150, #241, #303, zarr-developers#972,
zarr-developers#1977, zarr-developers#2434, zarr-developers#3711, zarr-developers#4305, zarr-developers#4307, zarr-developers#4328) because the layers disagreed on
this invariant and every span-derived chunk spelling clamped on its own:

- The metadata layer (common.py, metadata/v3.py) required chunk edges >= 1,
  but the in-memory FixedDimension allowed size == 0 with four special-case
  branches left over from zarr-developers#2434, so normalization could build a grid the
  metadata constructor then rejected. FixedDimension now rejects size < 1
  and the four `if self.size == 0` branches are gone. VaryingDimension
  already required edges > 0 and is unchanged.
- `chunks=-1`, `chunks=False`, `chunks="auto"` (_guess_regular_chunks, both
  the typesize == 0 early return and the np.maximum line) and `shards="auto"`
  each derived "one chunk covering the axis" independently. They now all go
  through one helper, `_full_span_chunk_size(span) = max(span, 1)`, which is
  the single definition of that phrase for a possibly zero-length axis.
- Zarr format 2 metadata had no chunk >= 1 check, so a legacy `chunks: [0]`
  document opened fine and read uninitialised memory after a resize. It now
  raises a clear ValueError at parse time, matching the format 3 grid.
- Rectilinear grids had no creation-time spelling for a zero-length axis:
  normalize_chunks_1d required sum(edges) == span, which no list of positive
  edges can satisfy for span 0, even though the same state is reachable via
  resize((0,)) and round-trips through reopen. For span == 0 any non-empty
  list of positive edges is now accepted verbatim, producing the same
  VaryingDimension(edges, extent=0) that resize produces; the strict sum
  check is kept for span > 0.

Tests: the per-spelling regression test from zarr-developers#4328 is replaced by one matrix
over {-1, False, "auto", 1, (1,...), [[2, 2]]} x {(0,), (0, 4), (4, 0),
(0, 0), ()} x {v2, v3} x {no shards, shards="auto" with and without a byte
budget, explicit shards}, with separate small tests for each error case.
Tests that constructed FixedDimension(size=0) now assert it raises, and a
zero-extent test covers the behaviour the old special cases were guarding.

Assisted-by: ClaudeCode:claude-fable-5-1
d-v-b added a commit to d-v-b/zarr-python that referenced this pull request Sep 9, 2026
…, clamps and metadata

Invariant: a chunk edge length is always >= 1; a dimension's extent may be 0,
in which case the dimension has zero chunks (ceildiv(0, size) == 0).

Zero-length-axis bugs have recurred since 2017 (#150, #241, #303, zarr-developers#972,
zarr-developers#1977, zarr-developers#2434, zarr-developers#3711, zarr-developers#4305, zarr-developers#4307, zarr-developers#4328) because the layers disagreed on
this invariant and every span-derived chunk spelling clamped on its own:

- The metadata layer (common.py, metadata/v3.py) required chunk edges >= 1,
  but the in-memory FixedDimension allowed size == 0 with four special-case
  branches left over from zarr-developers#2434, so normalization could build a grid the
  metadata constructor then rejected. FixedDimension now rejects size < 1
  and the four `if self.size == 0` branches are gone. VaryingDimension
  already required edges > 0 and is unchanged.
- `chunks=-1`, `chunks=False`, `chunks="auto"` (_guess_regular_chunks, both
  the typesize == 0 early return and the np.maximum line) and `shards="auto"`
  each derived "one chunk covering the axis" independently. They now all go
  through one helper, `_full_span_chunk_size(span) = max(span, 1)`, which is
  the single definition of that phrase for a possibly zero-length axis.
- Zarr format 2 metadata had no chunk >= 1 check, so a legacy `chunks: [0]`
  document opened fine and read uninitialised memory after a resize. It now
  raises a clear ValueError at parse time, matching the format 3 grid.
- Rectilinear grids had no creation-time spelling for a zero-length axis:
  normalize_chunks_1d required sum(edges) == span, which no list of positive
  edges can satisfy for span 0, even though the same state is reachable via
  resize((0,)) and round-trips through reopen. For span == 0 any non-empty
  list of positive edges is now accepted verbatim, producing the same
  VaryingDimension(edges, extent=0) that resize produces; the strict sum
  check is kept for span > 0.

Tests: the per-spelling regression test from zarr-developers#4328 is replaced by one matrix
over {-1, False, "auto", 1, (1,...), [[2, 2]]} x {(0,), (0, 4), (4, 0),
(0, 0), ()} x {v2, v3} x {no shards, shards="auto" with and without a byte
budget, explicit shards}, with separate small tests for each error case.
Tests that constructed FixedDimension(size=0) now assert it raises, and a
zero-extent test covers the behaviour the old special cases were guarding.

Assisted-by: ClaudeCode:claude-fable-5-1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant