Skip to content

feat: name packages that provide a codec zarr cannot find - #4277

Open
arcusbuilds wants to merge 2 commits into
zarr-developers:mainfrom
arcusbuilds:codec-package-hints
Open

feat: name packages that provide a codec zarr cannot find#4277
arcusbuilds wants to merge 2 commits into
zarr-developers:mainfrom
arcusbuilds:codec-package-hints

Conversation

@arcusbuilds

Copy link
Copy Markdown

Closes #4271.

When zarr cannot resolve a codec, the error repeats the name the user already has:

zarr.errors.UnknownCodecError: Unknown codec: 'vesuvius_rans'
numcodecs.errors.UnknownCodecError: codec not available: ''zfpy''

It does not say the failure is fixable, or which package fixes it. Since zarr and numcodecs both load codecs from entry points, pip install <package> is the whole fix, so naming the package is usually enough. Discussion #4269 is the case that prompted this.

Two tables in zarr/registry.py now map codec names to the packages that provide them:

An implementation for codec 'zfpy' is not available. Register one explicitly using the
codec registry (see https://numcodecs.readthedocs.io/...), or install a Python package
that registers a codec implementation with numcodecs. Known packages supporting this
codec: numcodecs[zfpy].

There are two tables because the formats use different registries and a name can mean different things in each. imagecodecs_* is virtual-tiff under zarr.codecs but imagecodecs-numcodecs under numcodecs.codecs. crc32c is built in for format 3 and needs numcodecs[crc32c] for format 2. Entries come from each package's declared entry points, and every distribution named resolves on PyPI. The tables also cover numcodecs' own optional extras (zfpy, pcodec, crc32c, msgpack2), which are the most common case in practice.

Backwards compatibility

get_codec_class raises UnknownCodecError instead of KeyError, both for an unregistered codec and for a codec whose configured implementation is not registered. get_numcodec raises it instead of the ValueError numcodecs raises. All are ValueError subclasses, so except ValueError is unaffected.

A KeyError cannot carry the message: its __str__ reprs the argument, so multi-sentence text comes back quoted and escaped. If you would rather keep the exception types, the tables and the message builder stand on their own and I can cut the rest.

numcodecs.errors only exists from 0.15.1 while the declared floor is 0.14, so the unregistered-codec check prefers that exception type where it can be imported and falls back to matching the message.

Testing

New tests/test_registry.py covers the tables, the message for both formats, both get_codec_class failure branches, get_numcodec, and two cases that open an array whose metadata names a missing codec. Full suite passes on numcodecs 0.16.5, and the files touched here also pass against numcodecs==0.14.1.

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>
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.14%. Comparing base (ce10c0b) to head (8a333bd).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4277      +/-   ##
==========================================
+ Coverage   94.12%   94.14%   +0.02%     
==========================================
  Files          92       92              
  Lines       12830    12869      +39     
==========================================
+ Hits        12076    12116      +40     
+ Misses        754      753       -1     
Files with missing lines Coverage Δ
src/zarr/core/metadata/v3.py 94.13% <100.00%> (+0.01%) ⬆️
src/zarr/errors.py 100.00% <ø> (ø)
src/zarr/metadata/migrate_v3.py 98.37% <100.00%> (+0.01%) ⬆️
src/zarr/registry.py 92.06% <100.00%> (+2.58%) ⬆️
🚀 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 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

this looks good, I had claude look for issues and it found some. Here's the summary:

🤖 AI text below 🤖

Review notes on this PR. Overall the feature is solid: the happy paths gain no overhead, the per-format table split is justified by the real imagecodecs_* name collision, and switching to codec_classes.get(config_entry) incidentally fixes what was previously dead code (if selected_codec_cls: could never be falsy). Findings below, roughly in order of importance.

1. parse_codecs no longer converts KeyError from from_dict (correctness regression).
The removed try/except KeyError in src/zarr/core/metadata/v3.py wrapped the whole expression get_codec_class(name_parsed).from_dict(c), not just the registry lookup. A third-party codec whose from_dict does raw dict indexing on malformed config now leaks a bare KeyError out of metadata parsing. Worse, on the zarr.open(store, mode="a", shape=...) path that KeyError is swallowed by the except (KeyError, NodeTypeValidationError) fallback in zarr/api/asynchronous.py (~line 412), so the user gets a nonsensical group-open error instead of a codec error. Both symptoms were reproduced against this branch. A narrow try/except KeyError around the .from_dict(c) call would restore the old behavior (the lookup half is now handled inside get_codec_class).

2. The config-pin branch raises the wrong exception type.
When zarr's config pins a codec to an implementation string that isn't registered, the new branch in get_codec_class raises UnknownCodecError — the exception this PR teaches users and tools means "codec missing, install a package". But the sibling getters in the same file (get_pipeline_class, get_buffer_class, get_ndbuffer_class) all raise BadConfigError for exactly this situation, and UnknownCodecError is documented as "raised when an unknown codec was used". This has a concrete consequence: migrate_v3._find_numcodecs_zarr3 catches UnknownCodecError and re-reports it as "Couldn't find corresponding zarr.codecs.numcodecs codec", so a config typo gets misdiagnosed during migration. BadConfigError is the consistent type here (still an improvement over the old bare KeyError).

3. Three tests depend on the hinted packages being absent.
test_get_numcodec_unknown_raises_with_package_hint and the two test_open_array_with_missing_*_codec_reports_package tests assume wavpack / n5_default are unregistered. Both registries are entry-point driven, so in any environment with wavpack-numcodecs or zarr-n5 installed — exactly the packages the error message advertises — the lookups succeed and pytest.raises fails. The neighboring get_codec_class tests already monkeypatch _codec_registries to a fresh defaultdict(Registry); these three need equivalent isolation (the numcodecs registry can be monkeypatched similarly).

4. test_mapping_does_not_shadow_builtin_codecs reads global state.
implemented = {name for name, reg in _codec_registries.items() if len(reg) > 0} conflates "loaded in this process" with "implemented by zarr itself". If an installed third-party entry-point codec (e.g. gribberish) was lazy-loaded by an earlier test, its name legitimately intersects the hint tables and the assertion fails even though nothing is shadowed; conversely, entry-point codecs still pending lazy_load have len(reg) == 0 and escape the check. Comparing against an explicit list of zarr's own codec names would make this deterministic.

5. Minor: get_numcodec's Raises section over-promises.
When data lacks a string "id", numcodecs' own UnknownCodecError ("codec not available: None") passes through unconverted, and it is not a subclass of zarr.errors.UnknownCodecError — a caller following the docstring with except zarr.errors.UnknownCodecError misses that case. The behavior is intentional (there's a test for it); the docstring just needs qualifying.

6. Minor: the # pragma: no cover - numcodecs < 0.15.1 is inaccurate.
The min_deps CI env pins numcodecs==0.14.* and runs run-coverage with codecov upload, so a coverage-measured job does execute that branch (via the new wavpack tests). The pragma can just be dropped.

Nits, take or leave: the new test file re-imports the same names (UnknownCodecError ×9, get_numcodec ×5, etc.) inside function bodies instead of the module-level import block; the three test_missing_codec_message_* functions and the _packages_for_codec tests could collapse into parametrized tables; and test_get_codec_class_unknown_raises_without_package_hint duplicates coverage that already exists in tests/test_config.py and tests/test_metadata/test_v3.py.

On the hand-maintained tables themselves: the pragmatic call is defensible (entry-point discovery is impossible for an uninstalled package), but the rows are unverifiable by CI and uncorrectable in shipped releases — if a listed name is ever abandoned and re-claimed on PyPI, released zarr error text keeps recommending it. Pointing the message at a docs page listing known codec packages would trade some in-terminal actionability for retroactive correctability; either choice works as long as it's made deliberately.

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>
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.

handle missing codecs by telling users what package to install to get the codec

2 participants