Skip to content

feat: add zarr-chunk-key-encoding package - #299

Open
d-v-b wants to merge 43 commits into
mainfrom
claude/zarr-chunk-key-encoding-pkg-ef7f13
Open

feat: add zarr-chunk-key-encoding package#299
d-v-b wants to merge 43 commits into
mainfrom
claude/zarr-chunk-key-encoding-pkg-ef7f13

Conversation

@d-v-b

@d-v-b d-v-b commented Aug 14, 2026

Copy link
Copy Markdown
Owner

🤖 AI text below 🤖

Adds a new standalone package, packages/zarr-chunk-key-encoding, providing the runtime behavior for Zarr v3 chunk key encodings, plus a small typing tweak in zarr-metadata that it relies on.

zarr-chunk-key-encoding

  • ChunkKeyEncoding abstract base class with encode / decode; decode is a first-class (optional) part of the interface and a strict inverse of encode — malformed or non-canonical keys ("c/01", "c/-1", wrong prefix) raise ChunkKeyDecodeError instead of being silently normalized.
  • Spec-defined DefaultChunkKeyEncoding and V2ChunkKeyEncoding.
  • Name dispatch over the closed set the v3 core spec defines (CHUNK_KEY_ENCODINGS). Deliberately no registration API and no entry point group: that machinery is common to all five v3 extension points (zarrs factors it into a shared zarrs_plugin crate), and zarr already scans its own zarr.chunk_key_encoding group, so a second one would be a competing plugin system plus a compatibility ratchet. Adding it later is additive.
  • chunk_key_encoding_from_json / parse_chunk_key_encoding for constructing encodings from JSON metadata or looser user input, built on the zarr-metadata types.
  • ChunkKey, a typing.NewType brand over str returned by encode — the static analogue of the validated StoreKey newtype in the zarrs Rust implementation. decode and membership testing still accept plain str, since their job is judging untrusted input.
  • BoundedChunkKeyEncoding / ChunkKeyEncoding.bind: an encoding restricted to a known chunk grid shape. Bounded decode is a total inverse of encode (resolving the v2 encoding's rank-zero ambiguity), and the valid key set is a finite collection — membership testing performs the full store-key check (grammar, rank, bounds, canonical spelling) in one test.
  • Full package scaffold: docs (mkdocs + readthedocs), justfile, towncrier changelog, py.typed, uv.lock.
  • 119 tests, plus a parity suite that compares byte-for-byte against zarr.core.chunk_key_encodings when zarr is importable (skipped otherwise; the package itself does not depend on zarr).

zarr-metadata

  • The four chunk key encoding TypedDicts now declare extra_items=JSONValue (PEP 728), making them assignable to Mapping[str, JSONValue] under type checkers that support extra-items TypedDicts. No runtime change; change note included.

CI

  • New zarr-chunk-key-encoding workflow matching the other subpackages: test (3.11–3.14) / ruff / pyright / docs jobs delegating to the package justfile, plus a complete gate.
  • Adds a parity job on top of that template. tests/test_zarr_parity.py importorskips zarr (deliberately not a dependency), so the suite would never have run anywhere; the job layers the repo-root environment in, and ZARR_PARITY_REQUIRED turns a missing zarr into a hard failure rather than a green run that tested nothing. 30 parity tests now actually execute.
  • Release workflow mirroring zarr-metadata-release.yml (tag-triggered, OIDC trusted publishing, attestations). Inert until a zarr_chunk_key_encoding-v* tag is pushed; the PyPI environments still need creating.
  • Registered the package with check_changelogs, and added the sdist allowlist the other subpackages grew in fix(build): use an sdist allowlist so the zarr sdist stops shipping subpackages zarr-developers/zarr-python#4261/fix(build): add an sdist allowlist to zarr-http-server zarr-developers/zarr-python#4262 — the sdist was previously shipping .venv and tool caches.

Test plan

  • just test in the package: 119 passed, 1 skipped (parity file skips without zarr).
  • Pre-commit (ruff, mypy, codespell, numpydoc, etc.) passes on both commits.

🤖 Generated with Claude Code

dependabot Bot and others added 16 commits May 31, 2026 19:28
…#176)

Bumps the actions group with 8 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [prefix-dev/setup-pixi](https://github.com/prefix-dev/setup-pixi) | `0.9.5` | `0.9.6` |
| [codecov/codecov-action](https://github.com/codecov/codecov-action) | `6.0.0` | `6.0.1` |
| [github/issue-metrics](https://github.com/github/issue-metrics) | `4.2.2` | `4.2.7` |
| [j178/prek-action](https://github.com/j178/prek-action) | `2.0.3` | `2.0.4` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.0` | `7.0.1` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `7.0.0` | `8.0.1` |
| [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.13.0` | `1.14.0` |
| [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) | `0.5.3` | `0.5.6` |



Updates `prefix-dev/setup-pixi` from 0.9.5 to 0.9.6
- [Release notes](https://github.com/prefix-dev/setup-pixi/releases)
- [Commits](prefix-dev/setup-pixi@1b2de7f...5185adf)

Updates `codecov/codecov-action` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@57e3a13...e79a696)

Updates `github/issue-metrics` from 4.2.2 to 4.2.7
- [Release notes](https://github.com/github/issue-metrics/releases)
- [Commits](github-community-projects/issue-metrics@c9e9838...1e38d5e)

Updates `j178/prek-action` from 2.0.3 to 2.0.4
- [Release notes](https://github.com/j178/prek-action/releases)
- [Commits](j178/prek-action@6ad8027...bdca6f1)

Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v7...043fb46)

Updates `actions/download-artifact` from 7.0.0 to 8.0.1
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v7...3e5f45b)

Updates `pypa/gh-action-pypi-publish` from 1.13.0 to 1.14.0
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](pypa/gh-action-pypi-publish@v1.13.0...cef2210)

Updates `zizmorcore/zizmor-action` from 0.5.3 to 0.5.6
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](zizmorcore/zizmor-action@b1d7e1f...5f14fd0)

---
updated-dependencies:
- dependency-name: prefix-dev/setup-pixi
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: github/issue-metrics
  dependency-version: 4.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: j178/prek-action
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/download-artifact
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: pypa/gh-action-pypi-publish
  dependency-version: 1.14.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: zizmorcore/zizmor-action
  dependency-version: 0.5.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…icts

The chunk key encoding TypedDicts now declare extra_items=JSONValue
(PEP 728), making them assignable to Mapping[str, JSONValue] under
type checkers that support extra-items TypedDicts. No runtime change.

Assisted-by: ClaudeCode:claude-fable-5
Initial release of zarr-chunk-key-encoding: the ChunkKeyEncoding
abstract base class, the spec-defined DefaultChunkKeyEncoding and
V2ChunkKeyEncoding, a name-keyed registry with entry-point discovery
modeled on the zarrs plugin registry, and JSON round-tripping built on
the zarr-metadata types.

Assisted-by: ClaudeCode:claude-fable-5
d-v-b added 13 commits August 15, 2026 23:21
Add BoundedChunkKeyEncoding and ChunkKeyEncoding.bind, restricting an
encoding to a known chunk grid shape. Bounded encode/decode reject
out-of-grid coordinates and keys via the new ChunkCoordsOutOfBoundsError
and ChunkKeyOutOfBoundsError, bounded decode is a total inverse of
encode (resolving the v2 encoding's rank-zero ambiguity), and the valid
key set is a finite collection supporting membership testing, iteration,
and len. Membership performs the full store-key check - grammar, rank,
bounds, canonical spelling - in one test, for consumers like
zarr-http-server that validate candidate keys against an array.

Assisted-by: ClaudeCode:claude-fable-5
…type

encode now returns ChunkKey, a typing.NewType brand over str - the
static analogue of the validated StoreKey newtype in the zarrs Rust
implementation. One brand serves the whole package rather than one per
encoding: keys are interchangeable at the store layer, and per-class
brands could not distinguish same-class instances with different
separators anyway. decode and membership testing still accept plain
str, since their job is judging untrusted input.

Assisted-by: ClaudeCode:claude-fable-5
Match the CI the other subpackages have. The test/ruff/pyright/docs jobs
delegate to the package justfile, mirroring zarr-metadata, and the matrix
covers the declared 3.11-3.14 range.

Adds a parity job on top of that template: tests/test_zarr_parity.py
importorskips zarr, which the package deliberately does not depend on, so
the suite would never have run anywhere. The job layers the repo-root
environment in via just test-parity, and ZARR_PARITY_REQUIRED turns a
missing zarr into a hard failure rather than a green run that tested
nothing.

Also register the package with check_changelogs, and add the sdist
allowlist the other subpackages grew in zarr-developers#4261/zarr-developers#4262 so releases stop
shipping .venv and tool caches.

Assisted-by: ClaudeCode:claude-fable-5
…ragments

The parity job resolves the sibling zarr-metadata as an editable overlay
against this package's zarr-metadata>=0.4.0 floor. hatch-vcs derives that
overlay's version from tags, so the default shallow checkout built it as
0.1.dev1 and resolution failed; fetch-depth: 0 makes it the real
post-0.4.0 version.

Rename the changelog fragments from towncrier's +no-issue form to the
299.<type>.md form ci/check_changelog_entries.py requires, matching the
other subpackages.

Assisted-by: ClaudeCode:claude-fable-5
Resolving this package's zarr-metadata>=0.4.0 floor against an editable
sibling makes the run depend on hatch-vcs finding zarr_metadata-v* tags.
A clone without them - a fork whose tags were never pushed, which is
exactly where this PR's CI runs - derives a pre-0.4.0 version and the
resolution fails. Take zarr-metadata from the index instead, which also
checks this package against the released zarr-metadata it declares;
lockstep development of the two packages is what the test recipe covers.

Assisted-by: ClaudeCode:claude-fable-5
Model the setup on zarr-metadata. The package already carried an RTD
config, but with the pre-zarr-developers#4252 build command that points `-f` at the
config from the repo root; mkdocs resolves some settings relative to the
working directory, so that silently looks for them in the wrong place.
Build from the package directory instead, making the RTD build identical
to `just docs-check` and the CI job.

Register the package in the root docs subprojects grid and the README
sub-package list, alongside the other companion packages.

Assisted-by: ClaudeCode:claude-fable-5
…odings

Chunk key encoding is a v3 extension point, so the registry is open-ended
and a consumer needs to know what it is being handed. Add
ChunkKeyEncodingSupport - CORE (defined by the core spec), EXTENSION
(registered in zarr-extensions), CUSTOM (neither, and the default) -
declared as a class attribute so entry-point registrations carry their own
provenance and holders of an instance can gate without a registry lookup.

registered_chunk_key_encodings takes a support= filter, usable directly as
a gating allowlist, and get_chunk_key_encoding_support looks one up.
Registering a class that claims CORE for a name the spec does not define
raises, so the tier a consumer gates on is not self-asserted by the code
being gated.

This matters more here than at most extension points: the spec does not
support must_understand=false for chunk key encodings, so a reader that
meets an unknown one must fail rather than ignore it.

Assisted-by: ClaudeCode:claude-fable-5
Addresses roborev findings on the branch.

Discovery runs lazily from any lookup that misses, so raising on a bad
entry point turned one unrelated third-party package's breakage into a
hard error on every such lookup - and because the loaded flag was set
before the scan, the entry points enumerated after the bad one were
stranded with no retry. Skip broken entry points with a new
ChunkKeyPluginWarning instead, covering import failure and non-subclass
loads as well, and set the flag only after a completed pass.

Discovery now also routes through register_chunk_key_encoding rather than
assigning to the registry, closing a hole where an entry point could
bypass its checks - notably by claiming the CORE support level for a name
the spec does not define.

Adds the entry-point tests that were missing entirely, and corrects the
sdist allowlist comment, which was inherited from zarr-metadata and
claimed the bundled tests and docs are runnable from an unpacked sdist;
every recipe here layers in the sibling zarr-metadata, which is not there.

Assisted-by: ClaudeCode:claude-fable-5
…gistry

Second roborev pass: only test_entry_points isolated the process-wide
registry, so any other test whose lookup misses triggered a real scan of
the installed zarr_chunk_key_encoding entry point group. In an environment
that ships one, that would register it mid-suite - or, if it were broken,
raise a ChunkKeyPluginWarning that this package's filterwarnings=error
turns into a failure in a test that has nothing to do with plugins.

Move the isolation into a conftest autouse fixture covering all modules,
and have it stub entry point discovery to find nothing by default; the
discovery tests override it, which is what they were already doing.

Assisted-by: ClaudeCode:claude-fable-5
Third roborev pass found the first; investigating it surfaced the second.

name is an un-defaulted ClassVar on the ABC, so a genuine ChunkKeyEncoding
subclass that forgets it passed the subclass guard and then raised
AttributeError on the attribute access - outside the try, and so back out
through discovery, breaking every later lookup. That is precisely the
fault isolation the previous commit added, so treat a missing or
non-string name as another skip-with-warning case.

Separately, ChunkKeyEncodingSupport is a StrEnum, so a class declaring
support = "core" as a plain string compared equal to CORE for every
consumer while failing the identity check that guards the tier: it
registered unvalidated and then reported itself as core. Require a real
enum member. Both were confirmed against the old code before fixing, and
both now have regression tests.

Assisted-by: ClaudeCode:claude-fable-5
…d seam

Every Zarr v3 extension point needs the same registry mechanics, and the
zarrs Rust implementation already factors them into a zarrs_plugin crate
that each extension-point crate depends on. Prepare for the same split
here, without yet adding a package.

_registry_core holds the generic half - name-keyed registration, lazy
entry-point discovery with fault isolation, and support-level validation -
parameterized by the things that differ per extension point, and imports
nothing from the rest of the package. registry.py keeps the public API as
a thin facade over one instance, alongside the chunk-key-specific coercion
that never generalizes. Extraction later is then a file move plus an
import change, with no public API break.

The state moves from module globals onto the instance, which pays off now
rather than only later: conftest was monkeypatching three module globals
to isolate tests, and the entry-point tests reached into module internals
to stub discovery. Both now work through the instance.

Assisted-by: ClaudeCode:claude-fable-5
…ion paths

Roborev: entry-point discovery rejected a subclass that never defines a
string name, but registering one directly fell through to the registry
lookup and raised a bare AttributeError instead of a package error.

Move the check into Registry.register, the chokepoint both paths share,
rather than duplicating it - discovery now reaches it through the register
call it already wraps, and uses getattr for its de-duplication test so a
nameless class gets there at all. It runs first because every later
message interpolates cls.name.

Assisted-by: ClaudeCode:claude-fable-5
d-v-b and others added 12 commits August 16, 2026 17:49
…w-safe

Roborev flagged the test/pyright/docs jobs as sharing the shallow-checkout
hazard that forced test-parity to stop overlaying the editable sibling.
They do not, and all three pass on tag-less checkouts today: uv sync
resolves the zarr-metadata>=0.4.0 floor from the index first (the job log
shows zarr-metadata==0.4.0 installed), and --with-editable then overlays
the local build without re-resolving, so the overlay's tag-derived version
never has to satisfy the floor. test-parity resolved against the repo root
instead, which did force the overlay through the floor.

Same ingredients, different resolution path - which is exactly why it reads
as a bug twice over. Write the distinction down rather than adding
fetch-depth: 0 to six jobs for a hazard they do not have.

Assisted-by: ClaudeCode:claude-fable-5
…d-set dispatch

Chunk key encoding is a v3 extension point, but the machinery third-party
encodings need - registration, entry-point discovery, and provenance tiers -
is identical for all five extension points, and zarrs factors exactly that
into a shared zarrs_plugin crate. Building it per-package is how the
duplication starts. zarr already scans a zarr.chunk_key_encoding entry point
group of its own, so shipping ours would create two competing plugin systems
for the same extension point, and an entry point group name is a
compatibility ratchet the moment a third party publishes against it.

So: no registry, no entry points, no support tiers until the shared package
exists. Everything removed can be added back compatibly; almost none of it
could be removed compatibly after release.

Name dispatch survives, over a closed mapping of the two spec-defined
encodings. chunk_key_encoding_from_json and parse_chunk_key_encoding keep
the signatures an open set would use, so growing into a registry later is
additive.

Removes _registry_core, registry, and support (with their tests and docs
pages) in favour of a from_json module; UnknownChunkKeyEncodingError now
reports known rather than registered encodings.

Assisted-by: ClaudeCode:claude-fable-5
Second time review has flagged this as a missing fetch-depth: 0. The job
passes on a shallow checkout - root zarr builds and all 30 parity tests
run - because nothing constrains zarr's version, so a tag-less hatch-vcs
fallback is harmless. The failure it resembles was zarr-metadata's derived
version having to clear a >=0.4.0 floor, which is constraint satisfaction
rather than version derivation, and test-parity now takes zarr-metadata
from the index so it cannot recur.

Assisted-by: ClaudeCode:claude-fable-5
Two bugs in parse_named_config_json, both confirmed against the old code.

must_understand: false was accepted and ignored, on the reasoning that this
package understands the encodings it constructs. That misses the point: the
v3 spec does not support must_understand: false for chunk key encodings at
all, precisely because a reader that meets an unrecognized one has to fail
rather than skip it, so the field is meaningless here and a document
carrying it is malformed. Reject it; keep accepting true.

Unexpected keys of mixed types raised a bare TypeError from the sorted()
call building the error message - sorting 1 against "zz" - so a malformed
document escaped the hierarchy the README promises everything derives from.
Sort by repr instead. The comment claiming non-string keys would surface in
the extra-keys check was only true when they were homogeneous.

Assisted-by: ClaudeCode:claude-fable-5
Rename abc, bounded, default, errors, from_json, separator and v2 to
leading-underscore names, so __all__ in the top-level namespace is the
entire public API and submodule paths stop being part of the contract.
_parsing was already private.

The API docs followed: seven per-module pages that would now be
documenting private modules collapse into one page rendering the package
namespace, and the cross-references in the narrative docs drop the module
segment, so a reader is only ever shown the path they should import from.

Two tests pin the result - every module carries the underscore, and every
name in __all__ resolves - because a module added without one would widen
the public surface silently.

Assisted-by: ClaudeCode:claude-fable-5
Replace 114 RST double-backtick literals with markdown code spans across
the seven modules. That was the only RST markup present - no literal-block
markers, interpreted-text roles, directives or RST hyperlinks - so the
change is purely inline.

The numpydoc section structure stays: Parameters/Returns/Raises are the
convention the numpydoc-validation hook enforces and the sibling packages
use, and mkdocstrings pairs numpy sections with markdown content, so the
two are not in tension.

Verified in the rendered HTML rather than assumed: every converted literal
comes out as a code span, and the only raw backticks left are inside the
verbatim source view, where docstrings and comments are shown as written.

Assisted-by: ClaudeCode:claude-fable-5
The module docstring claimed BoundedChunkKeyEncoding has no JSON form. That
conflated two things: it is not a Zarr v3 metadata extension - no name, and
an array's chunk_key_encoding field cannot hold one, since the grid shape
lives elsewhere in the array metadata - but it does have a serialization
of its own, {"grid_shape": [...], "chunk_key_encoding": ...}, so a consumer
can persist or transmit the bound object as a unit.

Add to_json / from_json, typed as a closed BoundedChunkKeyEncodingJSON.
from_json validates the envelope strictly, accepts a tuple or list grid
shape and either form of nested encoding metadata, and reuses the existing
grid-shape and encoding validation rather than duplicating it. The nested
dispatch is imported at call time to keep the module graph acyclic.

Assisted-by: ClaudeCode:claude-fable-5
bind said nothing about what was being bound. bind_grid_shape names its
one argument, and uses the same vocabulary as the grid_shape attribute,
the constructor parameter, and the JSON key, so a reader never has to look
it up.

Considered and rejected: as_bounded (as_* conventionally means a cheap view
of the same object; this builds a new object of a different type),
into_bounded (Rust's consuming-conversion connotation, false here - the
encoding stays usable), and with_bounding. All three name the result rather
than the operation, and the result type is the part the caller already
knows.

Assisted-by: ClaudeCode:claude-fable-5
Naming by result is the convention - to_json sits on the same class - and
"convert a to b" reads better than "convert a using a parameter of a to b".
The previous rename fixed the real defect in bind, which named neither the
operation nor the result, but over-corrected into naming the argument.

Assisted-by: ClaudeCode:claude-fable-5
…nded

The named counterpart to ChunkKeyEncoding.to_bounded: the same conversion
approached from the bounded side, and a named constructor to sit alongside
from_json. It is now the one place the construction lives - to_bounded and
from_json both delegate to it - so the two directions cannot drift apart.

Assisted-by: ClaudeCode:claude-fable-5
… style pass - #310 (zarr-developers#4296)

* docs: clarify gzip MTIME handling in comparison helper and fused-pipeline test docs

Follow-up to zarr-developers#4270: document that
_gzip_streams_equal_except_mtime skips bytes 4-8 (the RFC 1952 MTIME
field of the standard 10-byte gzip header), and update the
AsyncChunkTransform section comment and test docstring to note that
byte-identity is expected except for gzip's MTIME header field.

Assisted-by: ClaudeCode:claude-fable-5

* test: style pass on fused-pipeline tests

Hoist the imports that nearly every test re-imported locally, add a
shared _make_spec helper to replace nine copies of the ArraySpec
boilerplate, and convert test_async_chunk_transform_matches_sync to
Expect cases where each case declares its expected byte-comparison
function (exact equality, or gzip-MTIME-tolerant), removing the
isinstance branch from the test body.

Also add the missing read-back assertion in
test_sync_write_async_read_roundtrip, which previously read into a
buffer and never compared it to the written data.

Assisted-by: ClaudeCode:claude-fable-5

* test: require a ZDType in _make_spec instead of a dtype string

Assisted-by: ClaudeCode:claude-fable-5
…opers#4277)

* feat: name packages that provide a codec zarr cannot find

Closes zarr-developers#4271.

When zarr fails to resolve a codec it now says which Python packages are known to
provide it, instead of raising a bare KeyError holding only the codec name:

    An implementation for codec 'wavpack' is not available. Register one explicitly
    using the codec registry (see <docs>), or install a Python package that
    registers a codec implementation with numcodecs. Known packages supporting this
    codec: wavpack-numcodecs.

Two hand-maintained tables in zarr/registry.py hold the mapping, one per Zarr
format, because the two formats resolve codecs through different registries and the
same name can mean different things in each: `imagecodecs_*` names are declared by
`virtual-tiff` under the `zarr.codecs` entry point group and by
`imagecodecs-numcodecs` under `numcodecs.codecs`, and `crc32c` is a codec zarr
implements itself in format 3 while in format 2 it needs `numcodecs[crc32c]`. Each
table has an exact-match and a prefix-match half, since packages that provide many
codecs namespace them behind a shared prefix. Entries cover third-party packages and
the codecs numcodecs gates behind its own optional dependencies -- `zfpy`, `pcodec`,
`crc32c` and `msgpack2` -- which are the most common missing-codec case in practice.

Backwards compatibility: `get_codec_class` now raises `zarr.errors.UnknownCodecError`
instead of `KeyError`, both for a codec with no registered implementation and for a
codec whose configured implementation is not registered. `get_numcodec` raises it
instead of the ValueError numcodecs raises for an unregistered format 2 codec id.
All are subclasses of `ValueError`. Carrying the message on a `KeyError` was not an
option: `KeyError.__str__` reprs its argument, so a multi-sentence message comes back
quoted and escaped. `UnknownCodecError` is now exported from `zarr.errors`, since
users are being told to catch it.

`get_numcodec` supports numcodecs down to the declared 0.14 floor: `numcodecs.errors`
only exists from 0.15.1, so the unregistered-codec check prefers that exception type
where it is importable and falls back to matching the message otherwise.

Signed-off-by: arcusbuilds <srijankeshri007@gmail.com>

* fix: address review feedback

1. parse_codecs converts KeyError from from_dict again. The removed try/except
   wrapped the whole expression, not just the registry lookup, so a codec whose
   from_dict indexes a malformed configuration leaked a bare KeyError out of
   metadata parsing. On the zarr.open fallback path that KeyError was swallowed
   and reported as an unrelated group error: with mode="a" it surfaced as
   `TypeError: open_group() got an unexpected keyword argument 'shape'`.

   The catch is narrow, around from_dict only, since get_codec_class now raises
   for the lookup half. It raises MetadataValidationError naming the codec and
   the missing key rather than restoring the old message, which reported the
   missing configuration key as though it were the codec name
   ("Unknown codec: 'required_option'").

2. The config-pin branch raises BadConfigError, matching get_pipeline_class,
   get_buffer_class and get_ndbuffer_class, which all use it for this exact
   situation. This also stops migrate_v3._find_numcodecs_zarr3 misreporting a
   config typo as a missing numcodecs codec.

3. Three tests assumed the advertised packages were absent. Both registries are
   entry-point driven, so they failed in any environment with zarr-n5 or
   wavpack-numcodecs installed, which are the packages the messages recommend.
   Two fixtures now remove the specific entry for the duration of the test.
   Verified by installing both packages and re-running.

4. test_mapping_does_not_shadow_builtin_codecs selected on "registry is
   non-empty", conflating loaded-in-this-process with implemented-by-zarr. It
   now selects on the implementing class's module, so a lazy-loaded third-party
   codec cannot fail it.

5. get_numcodec's Raises section notes that numcodecs' own error propagates
   unchanged when data carries no string "id".

6. Dropped the `pragma: no cover` on the numcodecs < 0.15.1 fallback. The
   min_deps env pins numcodecs==0.14.* and runs run-coverage, so that branch is
   measured.

Also hoisted the repeated in-function imports in tests/test_registry.py to the
module level.

Signed-off-by: arcusbuilds <srijankeshri007@gmail.com>

* fix: address the second review round

Four defects, all found by review after the previous round was reported clean.

get_numcodec no longer wraps the numcodecs call in an exception handler. Catching
cannot distinguish "this id is unregistered" from "a registered codec rejected its
configuration" or "a wrapper codec failed to resolve an inner codec", and it was
relabelling both of the latter with the outer id plus a package hint that was
wrong. Reproduced: a wrapper registered as `wavpack` whose from_config resolved a
missing inner codec reported "An implementation for codec 'wavpack' is not
available ... install wavpack-numcodecs", when wavpack was installed and the
missing codec was something else entirely.

It now performs the lookups numcodecs performs, before delegating. That also fixes
reading `id` off a non-mapping input, which raised AttributeError where a
ValueError used to propagate. And it removes _is_missing_numcodec_error, and with
it the numcodecs <0.15.1 compatibility branch, since there is no longer an
exception to classify. Note this narrows the zarr error to Mapping inputs; a
duck-typed mapping now gets numcodecs' error instead, as it did before this PR.

The imagecodecs_ prefix in the Zarr format 3 table pointed at virtual-tiff, which
declares 15 of the 81 imagecodecs_* names under zarr.codecs; imagecodecs-numcodecs
declares all 81, but under numcodecs.codecs. Users of the other 66 names were told
to install a package that does not provide them. The format 3 side now lists the
15 exact names, so the rest get no hint rather than a wrong one. The format 2 side
keeps the prefix, where it is correct.

test_parse_codecs_converts_keyerror_from_from_dict leaked test_picky into the
global codec registry, in the file that also reads that global.

Tests: mutation testing showed six mutations surviving. Added the missing coverage
for a registered codec rejecting its configuration, wrapper codecs resolving an
inner codec by either route, non-mapping input, the imagecodecs_ over-match, and
the _resolve_codec entry point. Message assertions now pin the whole string and the
URL constants rather than substrings, which had allowed both documentation URLs to
be replaced with wrong ones and half the message body to be deleted with every test
still passing.

Also corrects the docs path to src/zarr/registry.py, and rewrites the changelog to
lead with the exception-type changes and to document the parse_codecs change it had
omitted.

Signed-off-by: arcusbuilds <srijankeshri007@gmail.com>

---------

Signed-off-by: arcusbuilds <srijankeshri007@gmail.com>
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>
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.

2 participants