From 5af3280af7f5c22199cf4d1d2538206840a07c79 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 13:03:09 +0200 Subject: [PATCH 01/15] docs(spec): deprecate BloscShuffle and BloscCname enums Design for steering BloscCodec users toward literal-string parameters, with the enum classes kept importable but deprecated on member access. Canonical: https://gist.github.com/d-v-b/9fd3fe92f82a24c929129f42a6f11f60 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...2026-05-10-deprecate-blosc-enums-design.md | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md diff --git a/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md b/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md new file mode 100644 index 0000000000..d29618f533 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md @@ -0,0 +1,112 @@ +> **Canonical source:** https://gist.github.com/d-v-b/9fd3fe92f82a24c929129f42a6f11f60 +> This file is a local cache. Edit the gist; sync the file from the gist (or push local edits with `gh gist edit 9fd3fe92f82a24c929129f42a6f11f60 -f 2026-05-10-deprecate-blosc-enums-design.md `). + +# Deprecate `BloscShuffle` and `BloscCname` enums + +## Goal + +Steer users from `BloscShuffle` / `BloscCname` enum members toward the equivalent literal strings (`"shuffle"`, `"zstd"`, etc.) when constructing a `BloscCodec`. The enum classes remain importable, but accessing a member emits a `DeprecationWarning`. Internal storage of the codec, and the public `cname` / `shuffle` attributes, become literal strings. + +## Out of scope + +- Removing the `BloscShuffle` / `BloscCname` classes. Deletion is deferred to a future major release. +- Other codecs that accept enum-style parameters (Zstd, etc.). + +## User-facing surface after the change + +- `BloscCodec(cname="zstd", shuffle="bitshuffle")` — preferred form, no warning. +- `BloscCodec(cname=BloscCname.zstd)` — works, but emits `DeprecationWarning` from the enum-member access *and* from the codec init (the access warning fires first; the init normalization treats the resolved string the same as a direct string). +- `from zarr.codecs import BloscShuffle, BloscCname` — silent (no warning on import). +- `BloscShuffle.shuffle` (member access) — `DeprecationWarning`, returns the string `"shuffle"`. +- `codec.cname`, `codec.shuffle` — return `str` (typed as `CName` / `Shuffle` literals), not enum members. + +## Design + +### Replace the enums with deprecation shims + +In [src/zarr/codecs/blosc.py](../../../src/zarr/codecs/blosc.py), replace `BloscShuffle(Enum)` and `BloscCname(Enum)` with classes whose metaclass intercepts attribute access: + +```python +class _DeprecatedStrEnumMeta(type): + _members: dict[str, str] + + def __getattr__(cls, name: str) -> str: + members = type.__getattribute__(cls, "_members") + if name in members: + warnings.warn( + f"{cls.__name__}.{name} is deprecated; pass the string " + f"{members[name]!r} instead.", + DeprecationWarning, + stacklevel=2, + ) + return members[name] + raise AttributeError(name) + + +class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): + _members = {"noshuffle": "noshuffle", "shuffle": "shuffle", "bitshuffle": "bitshuffle"} + + @staticmethod + def from_int(num: int) -> Shuffle: + mapping = {0: "noshuffle", 1: "shuffle", 2: "bitshuffle"} + if num not in mapping: + raise ValueError(f"Value must be between 0 and 2. Got {num}.") + return mapping[num] + + +class BloscCname(metaclass=_DeprecatedStrEnumMeta): + _members = { + "lz4": "lz4", "lz4hc": "lz4hc", "blosclz": "blosclz", + "zstd": "zstd", "snappy": "snappy", "zlib": "zlib", + } +``` + +Notes: +- `BloscShuffle.from_int` is now a `@staticmethod` returning a `str` (was a `@classmethod` returning an enum member). The only internal caller is `migrate_v3._convert_compressor`, which passes the result to `BloscCodec(shuffle=...)`. That call site needs no change because the codec already accepts strings. +- `_members` is read via `type.__getattribute__` to avoid recursing through `__getattr__`. +- No warning fires on `from zarr.codecs import BloscShuffle` — the class object is imported by name, not by member access. + +### Detect enum-shaped values in `BloscCodec.__init__` + +The init signature stays type-compatible (still accepts `BloscCname | CName` and `BloscShuffle | Shuffle | None`). The body changes to: + +1. If `cname` or `shuffle` is an `enum.Enum` instance, emit `DeprecationWarning("Passing a {BloscCname,BloscShuffle} enum to BloscCodec is deprecated; pass a literal string instead.")` and convert via `value.value`. (The new `BloscShuffle` / `BloscCname` shims never *produce* an enum instance — member access already returns a string — so this branch primarily catches code that pickled or otherwise materialized a real `enum.Enum` from the old definition.) +2. Replace `parse_enum(cname, BloscCname)` and `parse_enum(shuffle, BloscShuffle)` with explicit membership checks against the `SHUFFLE` / `CNAME` tuples; raise `ValueError` listing valid options on miss. +3. Internal stores: `typesize: int`, `cname: CName`, `clevel: int`, `shuffle: Shuffle`, `blocksize: int`. Update class-level annotations and the dataclass field declarations. + +### Downstream cleanup inside the file + +- `to_dict`: drop `.value`; emit `self.cname` / `self.shuffle` directly. +- `evolve_from_array_spec`: use string literals `"bitshuffle"` / `"shuffle"` instead of `BloscShuffle.bitshuffle`. +- `_blosc_codec`: rebuild the `map_shuffle_str_to_int` mapping with string keys; rebuild `cname` directly from `self.cname` (no `.name` needed). +- Update the docstring: `cname` / `shuffle` typed as literal strings; remove the `` example output; drop or rephrase the `See Also` section. + +### Internal call sites + +- [src/zarr/metadata/migrate_v3.py](../../../src/zarr/metadata/migrate_v3.py): no change needed beyond confirming `BloscShuffle.from_int(...)` still resolves. With `from_int` as a staticmethod on the new class body, attribute access on the class itself goes through `_DeprecatedStrEnumMeta.__getattr__`, which only intercepts `_members` keys — `from_int` is on the class normally and is not deprecated. +- Tests / docs that read `codec.cname.value` or compare against enum members must switch to string comparison. + +## Tests + +In [tests/test_codecs/test_blosc.py](../../../tests/test_codecs/test_blosc.py): + +1. Update `test_tunable_attrs_param`: + - The parametrized `BloscShuffle.shuffle` case must wrap codec construction in `pytest.warns(DeprecationWarning)`. + - Assertions like `codec.shuffle == BloscShuffle.bitshuffle` become `codec.shuffle == "bitshuffle"`. +2. New test: `BloscShuffle.shuffle` access raises `DeprecationWarning` and returns `"shuffle"`. Same for `BloscCname.zstd`. +3. New test: `BloscCodec(cname=BloscCname.zstd)` triggers `DeprecationWarning` and produces a codec with `codec.cname == "zstd"`. +4. New test: importing the names is silent (`with warnings.catch_warnings(record=True)` confirms no warning). + +## Documentation + +- [docs/quick-start.md](../../../docs/quick-start.md), [docs/user-guide/arrays.md](../../../docs/user-guide/arrays.md): replace each `zarr.codecs.BloscShuffle.` / `BloscCname.` with the literal string. +- `BloscCodec` docstring (in [src/zarr/codecs/blosc.py](../../../src/zarr/codecs/blosc.py)): drop enum-typed parameter docs in favor of literal string docs; remove the `` example output line. + +## Changelog + +Add `changes/.removal.md` containing one paragraph: "`BloscShuffle` and `BloscCname` enums are now deprecated. Pass the equivalent literal string (e.g. `'zstd'`, `'bitshuffle'`) when constructing `BloscCodec`. The enum classes remain importable but emit `DeprecationWarning` on member access; they will be removed in a future release." Use a placeholder filename like `0000.removal.md` and let the PR-creator rename it. + +## Risks + +- **Type-check fallout for downstream code** that annotates a variable as `BloscShuffle` and assigns it to `codec.shuffle`. After this change `codec.shuffle: str` so static checkers will flag the mismatch. This is the desired migration nudge, but worth calling out in the changelog. +- **`isinstance(codec.shuffle, BloscShuffle)` checks** in downstream code will silently start returning `False`. Same intent — those callers need to migrate — but harder to discover than a type error. From 74b2c1d2443b473486536307266b0c7060af2c27 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 22:46:40 +0200 Subject: [PATCH 02/15] feat(codecs): deprecate BloscShuffle/BloscCname enums Member access on BloscShuffle / BloscCname now emits DeprecationWarning and returns the equivalent string. BloscCodec stores cname/shuffle as literal strings; passing a real enum.Enum instance to __init__ warns. BloscShuffle.from_int returns a str. Internal call sites in migrate_v3 continue to work because BloscCodec accepts both forms. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zarr/codecs/blosc.py | 160 +++++++++++++++++++++----------- tests/test_codecs/test_blosc.py | 62 +++++++++++-- 2 files changed, 161 insertions(+), 61 deletions(-) diff --git a/src/zarr/codecs/blosc.py b/src/zarr/codecs/blosc.py index 62ceff7659..450e4b4c2d 100644 --- a/src/zarr/codecs/blosc.py +++ b/src/zarr/codecs/blosc.py @@ -1,10 +1,11 @@ from __future__ import annotations import asyncio +import warnings from dataclasses import dataclass, field, replace from enum import Enum from functools import cached_property -from typing import TYPE_CHECKING, Final, Literal, NotRequired, TypedDict +from typing import TYPE_CHECKING, ClassVar, Final, Literal, NotRequired, TypedDict import numcodecs from numcodecs.blosc import Blosc @@ -12,7 +13,7 @@ from zarr.abc.codec import BytesBytesCodec from zarr.core.buffer.cpu import as_numpy_array_wrapper -from zarr.core.common import JSON, NamedRequiredConfig, parse_enum, parse_named_configuration +from zarr.core.common import JSON, NamedRequiredConfig, parse_named_configuration from zarr.core.dtype.common import HasItemSize if TYPE_CHECKING: @@ -29,6 +30,8 @@ CName = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] """The codec identifiers used in the blosc codec """ +CNAME: Final = ("lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd") + class BloscConfigV2(TypedDict): """Configuration for the V2 Blosc codec""" @@ -56,38 +59,66 @@ class BloscJSON_V3(NamedRequiredConfig[Literal["blosc"], BloscConfigV3]): """ -class BloscShuffle(Enum): +class _DeprecatedStrEnumMeta(type): """ - Enum for shuffle filter used by blosc. + Metaclass for the legacy ``BloscShuffle`` / ``BloscCname`` classes. Accessing + a member name (e.g. ``BloscShuffle.bitshuffle``) emits a ``DeprecationWarning`` + and returns the equivalent string. """ - noshuffle = "noshuffle" - shuffle = "shuffle" - bitshuffle = "bitshuffle" + _members: dict[str, str] - @classmethod - def from_int(cls, num: int) -> BloscShuffle: - blosc_shuffle_int_to_str = { + def __getattr__(cls, name: str) -> str: + members: dict[str, str] = type.__getattribute__(cls, "_members") + if name in members: + warnings.warn( + f"{cls.__name__}.{name} is deprecated; pass the string {members[name]!r} instead.", + DeprecationWarning, + stacklevel=2, + ) + return members[name] + raise AttributeError(name) + + +class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): + """ + Deprecated. Pass a literal string (``"noshuffle"``, ``"shuffle"``, or + ``"bitshuffle"``) directly to :class:`BloscCodec` instead. + """ + + _members: ClassVar[dict[str, str]] = { + "noshuffle": "noshuffle", + "shuffle": "shuffle", + "bitshuffle": "bitshuffle", + } + + @staticmethod + def from_int(num: int) -> Shuffle: + mapping: dict[int, Shuffle] = { 0: "noshuffle", 1: "shuffle", 2: "bitshuffle", } - if num not in blosc_shuffle_int_to_str: + if num not in mapping: raise ValueError(f"Value must be between 0 and 2. Got {num}.") - return BloscShuffle[blosc_shuffle_int_to_str[num]] + return mapping[num] -class BloscCname(Enum): +class BloscCname(metaclass=_DeprecatedStrEnumMeta): """ - Enum for compression library used by blosc. + Deprecated. Pass a literal string (one of ``"lz4"``, ``"lz4hc"``, + ``"blosclz"``, ``"snappy"``, ``"zlib"``, ``"zstd"``) directly to + :class:`BloscCodec` instead. """ - lz4 = "lz4" - lz4hc = "lz4hc" - blosclz = "blosclz" - zstd = "zstd" - snappy = "snappy" - zlib = "zlib" + _members: ClassVar[dict[str, str]] = { + "lz4": "lz4", + "lz4hc": "lz4hc", + "blosclz": "blosclz", + "snappy": "snappy", + "zstd": "zstd", + "zlib": "zlib", + } # See https://zarr.readthedocs.io/en/stable/user-guide/performance.html#configuring-blosc @@ -118,6 +149,34 @@ def parse_blocksize(data: JSON) -> int: raise TypeError(f"Value should be an int. Got {type(data)} instead.") +def _coerce_enum_input(value: object, param_name: str) -> object: + """ + If ``value`` is a real :class:`enum.Enum` instance, emit a deprecation + warning and return ``value.value``. Otherwise return ``value`` unchanged. + """ + if isinstance(value, Enum): + warnings.warn( + f"Passing an enum to BloscCodec(..., {param_name}=...) is deprecated; " + "pass the equivalent literal string instead.", + DeprecationWarning, + stacklevel=3, + ) + return value.value + return value + + +def _parse_cname(data: object) -> CName: + if isinstance(data, str) and data in CNAME: + return data # type: ignore[return-value] + raise ValueError(f"cname must be one of {list(CNAME)!r}. Got {data!r}.") + + +def _parse_shuffle(data: object) -> Shuffle: + if isinstance(data, str) and data in SHUFFLE: + return data # type: ignore[return-value] + raise ValueError(f"shuffle must be one of {list(SHUFFLE)!r}. Got {data!r}.") + + @dataclass(frozen=True) class BloscCodec(BytesBytesCodec): """ @@ -133,12 +192,14 @@ class BloscCodec(BytesBytesCodec): Always False for Blosc codec, as compression produces variable-sized output. typesize : int The data type size in bytes used for shuffle filtering. - cname : BloscCname - The compression algorithm being used (lz4, lz4hc, blosclz, snappy, zlib, or zstd). + cname : str + The compression algorithm being used; one of `"lz4"`, `"lz4hc"`, + `"blosclz"`, `"snappy"`, `"zlib"`, or `"zstd"`. clevel : int The compression level (0-9). - shuffle : BloscShuffle - The shuffle filter mode (noshuffle, shuffle, or bitshuffle). + shuffle : str + The shuffle filter mode; one of `"noshuffle"`, `"shuffle"`, or + `"bitshuffle"`. blocksize : int The size of compressed blocks in bytes (0 for automatic). @@ -148,12 +209,13 @@ class BloscCodec(BytesBytesCodec): The data type size in bytes. This affects how the shuffle filter processes the data. If None, defaults to 1 and the attribute is marked as tunable. Default: 1. - cname : BloscCname or {'lz4', 'lz4hc', 'blosclz', 'snappy', 'zlib', 'zstd'}, optional - The compression algorithm to use. Default: 'zstd'. + cname : {'lz4', 'lz4hc', 'blosclz', 'snappy', 'zlib', 'zstd'}, optional + The compression algorithm to use. Default: `'zstd'`. Passing a + :class:`BloscCname` enum is deprecated. clevel : int, optional The compression level, from 0 (no compression) to 9 (maximum compression). Higher values provide better compression at the cost of speed. Default: 5. - shuffle : BloscShuffle or {'noshuffle', 'shuffle', 'bitshuffle'}, optional + shuffle : {'noshuffle', 'shuffle', 'bitshuffle'}, optional The shuffle filter to apply before compression: - 'noshuffle': No shuffling @@ -183,18 +245,13 @@ class BloscCodec(BytesBytesCodec): >>> codec.typesize 1 >>> codec.shuffle - + 'bitshuffle' Create a codec with specific compression settings: >>> codec = BloscCodec(cname='zstd', clevel=9, shuffle='shuffle') >>> codec.cname - - - See Also - -------- - BloscShuffle : Enum for shuffle filter options - BloscCname : Enum for compression algorithm options + 'zstd' """ # This attribute tracks parameters were set to None at init time, and thus tunable @@ -202,38 +259,37 @@ class BloscCodec(BytesBytesCodec): is_fixed_size = False typesize: int - cname: BloscCname + cname: CName clevel: int - shuffle: BloscShuffle + shuffle: Shuffle blocksize: int def __init__( self, *, typesize: int | None = None, - cname: BloscCname | CName = BloscCname.zstd, + cname: BloscCname | CName = "zstd", clevel: int = 5, shuffle: BloscShuffle | Shuffle | None = None, blocksize: int = 0, ) -> None: object.__setattr__(self, "_tunable_attrs", set()) - # If typesize was set to None, replace it with a valid typesize - # and flag the typesize attribute as safe to replace later if typesize is None: typesize = 1 self._tunable_attrs.update({"typesize"}) - # If shuffle was set to None, replace it with a valid shuffle - # and flag the shuffle attribute as safe to replace later if shuffle is None: - shuffle = BloscShuffle.bitshuffle + shuffle = "bitshuffle" self._tunable_attrs.update({"shuffle"}) + cname = _coerce_enum_input(cname, "cname") # type: ignore[assignment] + shuffle = _coerce_enum_input(shuffle, "shuffle") # type: ignore[assignment] + typesize_parsed = parse_typesize(typesize) - cname_parsed = parse_enum(cname, BloscCname) + cname_parsed = _parse_cname(cname) clevel_parsed = parse_clevel(clevel) - shuffle_parsed = parse_enum(shuffle, BloscShuffle) + shuffle_parsed = _parse_shuffle(shuffle) blocksize_parsed = parse_blocksize(blocksize) object.__setattr__(self, "typesize", typesize_parsed) @@ -252,9 +308,9 @@ def to_dict(self) -> dict[str, JSON]: "name": "blosc", "configuration": { "typesize": self.typesize, - "cname": self.cname.value, + "cname": self.cname, "clevel": self.clevel, - "shuffle": self.shuffle.value, + "shuffle": self.shuffle, "blocksize": self.blocksize, }, } @@ -276,20 +332,20 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: if "shuffle" in self._tunable_attrs: new_codec = replace( new_codec, - shuffle=(BloscShuffle.bitshuffle if item_size == 1 else BloscShuffle.shuffle), + shuffle=("bitshuffle" if item_size == 1 else "shuffle"), ) return new_codec @cached_property def _blosc_codec(self) -> Blosc: - map_shuffle_str_to_int = { - BloscShuffle.noshuffle: 0, - BloscShuffle.shuffle: 1, - BloscShuffle.bitshuffle: 2, + map_shuffle_str_to_int: dict[Shuffle, int] = { + "noshuffle": 0, + "shuffle": 1, + "bitshuffle": 2, } config_dict: BloscConfigV2 = { - "cname": self.cname.name, # type: ignore[typeddict-item] + "cname": self.cname, "clevel": self.clevel, "shuffle": map_shuffle_str_to_int[self.shuffle], "blocksize": self.blocksize, diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index 0201beb8de..401bcbdfce 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -1,4 +1,6 @@ +import enum import json +import warnings import numcodecs import numpy as np @@ -8,7 +10,7 @@ import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec -from zarr.codecs.blosc import BloscShuffle, Shuffle +from zarr.codecs.blosc import BloscCname, BloscShuffle, Shuffle from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype @@ -61,16 +63,24 @@ async def test_blosc_evolve(dtype: str) -> None: assert blosc_configuration_json["shuffle"] == "shuffle" -@pytest.mark.parametrize("shuffle", [None, "bitshuffle", BloscShuffle.shuffle]) +@pytest.mark.parametrize("shuffle", [None, "bitshuffle", "legacy-enum"]) @pytest.mark.parametrize("typesize", [None, 1, 2]) -def test_tunable_attrs_param(shuffle: None | Shuffle | BloscShuffle, typesize: None | int) -> None: +def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int) -> None: """ - Test that the tunable_attrs parameter is set as expected when creating a BloscCodec, + Test that the tunable_attrs parameter is set as expected when creating a BloscCodec. """ - codec = BloscCodec(typesize=typesize, shuffle=shuffle) + # Materialize BloscShuffle.shuffle via the deprecation shim without + # contaminating the BloscCodec construction below with that warning. + if shuffle == "legacy-enum": + with pytest.warns(DeprecationWarning, match="BloscShuffle.shuffle"): + shuffle_arg: None | Shuffle | str = BloscShuffle.shuffle + else: + shuffle_arg = shuffle + + codec = BloscCodec(typesize=typesize, shuffle=shuffle_arg) # type: ignore[arg-type] - if shuffle is None: - assert codec.shuffle == BloscShuffle.bitshuffle # default shuffle + if shuffle_arg is None: + assert codec.shuffle == "bitshuffle" # default shuffle assert "shuffle" in codec._tunable_attrs if typesize is None: assert codec.typesize == 1 # default typesize @@ -90,8 +100,8 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | BloscShuffle, typesize: N assert evolved_codec.typesize == new_dtype.item_size else: assert evolved_codec.typesize == codec.typesize - if shuffle is None: - assert evolved_codec.shuffle == BloscShuffle.shuffle + if shuffle_arg is None: + assert evolved_codec.shuffle == "shuffle" else: assert evolved_codec.shuffle == codec.shuffle @@ -135,3 +145,37 @@ def test_blosc_codec_sync_roundtrip() -> None: decoded = codec._decode_sync(encoded, spec) result = np.frombuffer(decoded.as_numpy_array(), dtype="float64") np.testing.assert_array_equal(arr, result) + + +def test_blosc_shuffle_member_access_warns() -> None: + with pytest.warns(DeprecationWarning, match="BloscShuffle.shuffle"): + value = BloscShuffle.shuffle + assert value == "shuffle" + + +def test_blosc_cname_member_access_warns() -> None: + with pytest.warns(DeprecationWarning, match="BloscCname.zstd"): + value = BloscCname.zstd + assert value == "zstd" + + +def test_blosc_enum_classes_import_silently() -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") + from zarr.codecs.blosc import BloscCname as _BloscCname # noqa: F401 + from zarr.codecs.blosc import BloscShuffle as _BloscShuffle # noqa: F401 + + +def test_blosc_codec_init_with_enum_instance_warns() -> None: + """A real enum.Enum instance must trigger the init-level deprecation warning.""" + + class LegacyShuffle(enum.Enum): + bitshuffle = "bitshuffle" + + class LegacyCname(enum.Enum): + zstd = "zstd" + + with pytest.warns(DeprecationWarning, match="enum"): + codec = BloscCodec(cname=LegacyCname.zstd, shuffle=LegacyShuffle.bitshuffle) # type: ignore[arg-type] + assert codec.cname == "zstd" + assert codec.shuffle == "bitshuffle" From dfaec375bde2d0d14ad2cfe7e08a21cd4f37599b Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 22:49:35 +0200 Subject: [PATCH 03/15] docs: use literal strings for BloscCodec shuffle parameter The BloscShuffle and BloscCname enums are deprecated; update doc examples to the recommended literal-string form. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/quick-start.md | 2 +- docs/user-guide/arrays.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/quick-start.md b/docs/quick-start.md index bb7a556b96..27dc8e6045 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -58,7 +58,7 @@ z = zarr.create_array( compressors=zarr.codecs.BloscCodec( cname="zstd", clevel=3, - shuffle=zarr.codecs.BloscShuffle.shuffle + shuffle="shuffle" ) ) diff --git a/docs/user-guide/arrays.md b/docs/user-guide/arrays.md index 6a0ecfda92..ef5548c391 100644 --- a/docs/user-guide/arrays.md +++ b/docs/user-guide/arrays.md @@ -201,7 +201,7 @@ Different compressors can be provided via the `compressors` keyword argument accepted by all array creation functions. For example: ```python exec="true" session="arrays" source="above" result="ansi" -compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=3, shuffle=zarr.codecs.BloscShuffle.bitshuffle) +compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=3, shuffle='bitshuffle') data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-5.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), compressors=compressors) z[:] = data @@ -298,7 +298,7 @@ Here is an example using a delta filter with the Blosc compressor: from zarr.codecs.numcodecs import Delta filters = [Delta(dtype='int32')] -compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=1, shuffle=zarr.codecs.BloscShuffle.shuffle) +compressors = zarr.codecs.BloscCodec(cname='zstd', clevel=1, shuffle='shuffle') data = np.arange(100000000, dtype='int32').reshape(10000, 10000) z = zarr.create_array(store='data/example-9.zarr', shape=data.shape, dtype=data.dtype, chunks=(1000, 1000), filters=filters, compressors=compressors) print(z.info_complete()) From d73cd5246d5ce7271f6372e91f00a1ef1555386c Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 22:49:53 +0200 Subject: [PATCH 04/15] chore(changes): add changelog entry for blosc enum deprecation The 0000 filename is a placeholder; rename to the PR number when the pull request is opened. Co-Authored-By: Claude Opus 4.7 (1M context) --- changes/0000.removal.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 changes/0000.removal.md diff --git a/changes/0000.removal.md b/changes/0000.removal.md new file mode 100644 index 0000000000..a83fd53853 --- /dev/null +++ b/changes/0000.removal.md @@ -0,0 +1,6 @@ +The ``BloscShuffle`` and ``BloscCname`` enums (``zarr.codecs.BloscShuffle``, +``zarr.codecs.BloscCname``) are now deprecated. Pass the equivalent literal +string (e.g. ``"zstd"``, ``"bitshuffle"``) when constructing a ``BloscCodec``. +The enum classes remain importable but emit ``DeprecationWarning`` on member +access, and will be removed in a future release. ``BloscCodec.cname`` and +``BloscCodec.shuffle`` are now plain strings rather than enum members. From 28d13c99bbdab21ada94c36b3294657837e882fd Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 23:05:22 +0200 Subject: [PATCH 05/15] docs: drop local copy of blosc enum deprecation spec The design doc is published as a public gist linked from the PR description; the in-tree copy is no longer needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...2026-05-10-deprecate-blosc-enums-design.md | 112 ------------------ 1 file changed, 112 deletions(-) delete mode 100644 docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md diff --git a/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md b/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md deleted file mode 100644 index d29618f533..0000000000 --- a/docs/superpowers/specs/2026-05-10-deprecate-blosc-enums-design.md +++ /dev/null @@ -1,112 +0,0 @@ -> **Canonical source:** https://gist.github.com/d-v-b/9fd3fe92f82a24c929129f42a6f11f60 -> This file is a local cache. Edit the gist; sync the file from the gist (or push local edits with `gh gist edit 9fd3fe92f82a24c929129f42a6f11f60 -f 2026-05-10-deprecate-blosc-enums-design.md `). - -# Deprecate `BloscShuffle` and `BloscCname` enums - -## Goal - -Steer users from `BloscShuffle` / `BloscCname` enum members toward the equivalent literal strings (`"shuffle"`, `"zstd"`, etc.) when constructing a `BloscCodec`. The enum classes remain importable, but accessing a member emits a `DeprecationWarning`. Internal storage of the codec, and the public `cname` / `shuffle` attributes, become literal strings. - -## Out of scope - -- Removing the `BloscShuffle` / `BloscCname` classes. Deletion is deferred to a future major release. -- Other codecs that accept enum-style parameters (Zstd, etc.). - -## User-facing surface after the change - -- `BloscCodec(cname="zstd", shuffle="bitshuffle")` — preferred form, no warning. -- `BloscCodec(cname=BloscCname.zstd)` — works, but emits `DeprecationWarning` from the enum-member access *and* from the codec init (the access warning fires first; the init normalization treats the resolved string the same as a direct string). -- `from zarr.codecs import BloscShuffle, BloscCname` — silent (no warning on import). -- `BloscShuffle.shuffle` (member access) — `DeprecationWarning`, returns the string `"shuffle"`. -- `codec.cname`, `codec.shuffle` — return `str` (typed as `CName` / `Shuffle` literals), not enum members. - -## Design - -### Replace the enums with deprecation shims - -In [src/zarr/codecs/blosc.py](../../../src/zarr/codecs/blosc.py), replace `BloscShuffle(Enum)` and `BloscCname(Enum)` with classes whose metaclass intercepts attribute access: - -```python -class _DeprecatedStrEnumMeta(type): - _members: dict[str, str] - - def __getattr__(cls, name: str) -> str: - members = type.__getattribute__(cls, "_members") - if name in members: - warnings.warn( - f"{cls.__name__}.{name} is deprecated; pass the string " - f"{members[name]!r} instead.", - DeprecationWarning, - stacklevel=2, - ) - return members[name] - raise AttributeError(name) - - -class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): - _members = {"noshuffle": "noshuffle", "shuffle": "shuffle", "bitshuffle": "bitshuffle"} - - @staticmethod - def from_int(num: int) -> Shuffle: - mapping = {0: "noshuffle", 1: "shuffle", 2: "bitshuffle"} - if num not in mapping: - raise ValueError(f"Value must be between 0 and 2. Got {num}.") - return mapping[num] - - -class BloscCname(metaclass=_DeprecatedStrEnumMeta): - _members = { - "lz4": "lz4", "lz4hc": "lz4hc", "blosclz": "blosclz", - "zstd": "zstd", "snappy": "snappy", "zlib": "zlib", - } -``` - -Notes: -- `BloscShuffle.from_int` is now a `@staticmethod` returning a `str` (was a `@classmethod` returning an enum member). The only internal caller is `migrate_v3._convert_compressor`, which passes the result to `BloscCodec(shuffle=...)`. That call site needs no change because the codec already accepts strings. -- `_members` is read via `type.__getattribute__` to avoid recursing through `__getattr__`. -- No warning fires on `from zarr.codecs import BloscShuffle` — the class object is imported by name, not by member access. - -### Detect enum-shaped values in `BloscCodec.__init__` - -The init signature stays type-compatible (still accepts `BloscCname | CName` and `BloscShuffle | Shuffle | None`). The body changes to: - -1. If `cname` or `shuffle` is an `enum.Enum` instance, emit `DeprecationWarning("Passing a {BloscCname,BloscShuffle} enum to BloscCodec is deprecated; pass a literal string instead.")` and convert via `value.value`. (The new `BloscShuffle` / `BloscCname` shims never *produce* an enum instance — member access already returns a string — so this branch primarily catches code that pickled or otherwise materialized a real `enum.Enum` from the old definition.) -2. Replace `parse_enum(cname, BloscCname)` and `parse_enum(shuffle, BloscShuffle)` with explicit membership checks against the `SHUFFLE` / `CNAME` tuples; raise `ValueError` listing valid options on miss. -3. Internal stores: `typesize: int`, `cname: CName`, `clevel: int`, `shuffle: Shuffle`, `blocksize: int`. Update class-level annotations and the dataclass field declarations. - -### Downstream cleanup inside the file - -- `to_dict`: drop `.value`; emit `self.cname` / `self.shuffle` directly. -- `evolve_from_array_spec`: use string literals `"bitshuffle"` / `"shuffle"` instead of `BloscShuffle.bitshuffle`. -- `_blosc_codec`: rebuild the `map_shuffle_str_to_int` mapping with string keys; rebuild `cname` directly from `self.cname` (no `.name` needed). -- Update the docstring: `cname` / `shuffle` typed as literal strings; remove the `` example output; drop or rephrase the `See Also` section. - -### Internal call sites - -- [src/zarr/metadata/migrate_v3.py](../../../src/zarr/metadata/migrate_v3.py): no change needed beyond confirming `BloscShuffle.from_int(...)` still resolves. With `from_int` as a staticmethod on the new class body, attribute access on the class itself goes through `_DeprecatedStrEnumMeta.__getattr__`, which only intercepts `_members` keys — `from_int` is on the class normally and is not deprecated. -- Tests / docs that read `codec.cname.value` or compare against enum members must switch to string comparison. - -## Tests - -In [tests/test_codecs/test_blosc.py](../../../tests/test_codecs/test_blosc.py): - -1. Update `test_tunable_attrs_param`: - - The parametrized `BloscShuffle.shuffle` case must wrap codec construction in `pytest.warns(DeprecationWarning)`. - - Assertions like `codec.shuffle == BloscShuffle.bitshuffle` become `codec.shuffle == "bitshuffle"`. -2. New test: `BloscShuffle.shuffle` access raises `DeprecationWarning` and returns `"shuffle"`. Same for `BloscCname.zstd`. -3. New test: `BloscCodec(cname=BloscCname.zstd)` triggers `DeprecationWarning` and produces a codec with `codec.cname == "zstd"`. -4. New test: importing the names is silent (`with warnings.catch_warnings(record=True)` confirms no warning). - -## Documentation - -- [docs/quick-start.md](../../../docs/quick-start.md), [docs/user-guide/arrays.md](../../../docs/user-guide/arrays.md): replace each `zarr.codecs.BloscShuffle.` / `BloscCname.` with the literal string. -- `BloscCodec` docstring (in [src/zarr/codecs/blosc.py](../../../src/zarr/codecs/blosc.py)): drop enum-typed parameter docs in favor of literal string docs; remove the `` example output line. - -## Changelog - -Add `changes/.removal.md` containing one paragraph: "`BloscShuffle` and `BloscCname` enums are now deprecated. Pass the equivalent literal string (e.g. `'zstd'`, `'bitshuffle'`) when constructing `BloscCodec`. The enum classes remain importable but emit `DeprecationWarning` on member access; they will be removed in a future release." Use a placeholder filename like `0000.removal.md` and let the PR-creator rename it. - -## Risks - -- **Type-check fallout for downstream code** that annotates a variable as `BloscShuffle` and assigns it to `codec.shuffle`. After this change `codec.shuffle: str` so static checkers will flag the mismatch. This is the desired migration nudge, but worth calling out in the changelog. -- **`isinstance(codec.shuffle, BloscShuffle)` checks** in downstream code will silently start returning `False`. Same intent — those callers need to migrate — but harder to discover than a type error. From c7a676ad357991c1784805a8d99f00c3874e8746 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sun, 10 May 2026 23:14:51 +0200 Subject: [PATCH 06/15] style(codecs): use plain backticks in blosc docstrings Replace Sphinx :class: roles and double-backticks with single backticks in the new docstrings added by the blosc enum deprecation. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zarr/codecs/blosc.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/zarr/codecs/blosc.py b/src/zarr/codecs/blosc.py index 450e4b4c2d..2ea90d8cb9 100644 --- a/src/zarr/codecs/blosc.py +++ b/src/zarr/codecs/blosc.py @@ -61,8 +61,8 @@ class BloscJSON_V3(NamedRequiredConfig[Literal["blosc"], BloscConfigV3]): class _DeprecatedStrEnumMeta(type): """ - Metaclass for the legacy ``BloscShuffle`` / ``BloscCname`` classes. Accessing - a member name (e.g. ``BloscShuffle.bitshuffle``) emits a ``DeprecationWarning`` + Metaclass for the legacy `BloscShuffle` / `BloscCname` classes. Accessing + a member name (e.g. `BloscShuffle.bitshuffle`) emits a `DeprecationWarning` and returns the equivalent string. """ @@ -82,8 +82,8 @@ def __getattr__(cls, name: str) -> str: class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): """ - Deprecated. Pass a literal string (``"noshuffle"``, ``"shuffle"``, or - ``"bitshuffle"``) directly to :class:`BloscCodec` instead. + Deprecated. Pass a literal string (`"noshuffle"`, `"shuffle"`, or + `"bitshuffle"`) directly to `BloscCodec` instead. """ _members: ClassVar[dict[str, str]] = { @@ -106,9 +106,9 @@ def from_int(num: int) -> Shuffle: class BloscCname(metaclass=_DeprecatedStrEnumMeta): """ - Deprecated. Pass a literal string (one of ``"lz4"``, ``"lz4hc"``, - ``"blosclz"``, ``"snappy"``, ``"zlib"``, ``"zstd"``) directly to - :class:`BloscCodec` instead. + Deprecated. Pass a literal string (one of `"lz4"`, `"lz4hc"`, + `"blosclz"`, `"snappy"`, `"zlib"`, `"zstd"`) directly to + `BloscCodec` instead. """ _members: ClassVar[dict[str, str]] = { @@ -151,8 +151,8 @@ def parse_blocksize(data: JSON) -> int: def _coerce_enum_input(value: object, param_name: str) -> object: """ - If ``value`` is a real :class:`enum.Enum` instance, emit a deprecation - warning and return ``value.value``. Otherwise return ``value`` unchanged. + If `value` is a real `enum.Enum` instance, emit a deprecation warning + and return `value.value`. Otherwise return `value` unchanged. """ if isinstance(value, Enum): warnings.warn( @@ -211,7 +211,7 @@ class BloscCodec(BytesBytesCodec): Default: 1. cname : {'lz4', 'lz4hc', 'blosclz', 'snappy', 'zlib', 'zstd'}, optional The compression algorithm to use. Default: `'zstd'`. Passing a - :class:`BloscCname` enum is deprecated. + `BloscCname` enum is deprecated. clevel : int, optional The compression level, from 0 (no compression) to 9 (maximum compression). Higher values provide better compression at the cost of speed. Default: 5. From a6ae2a4c64f9c7bfb16cb41f0fe0ba077ca37b77 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 00:03:22 +0200 Subject: [PATCH 07/15] test(codecs): cover blosc error branches Add tests for the ValueError paths in _parse_cname / _parse_shuffle and the AttributeError path in the deprecated-enum metaclass, which codecov reported as uncovered. Drop a few "type: ignore" markers that mypy now flags as unused after the init signature widened. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index 401bcbdfce..de37e2eef3 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -77,7 +77,7 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int else: shuffle_arg = shuffle - codec = BloscCodec(typesize=typesize, shuffle=shuffle_arg) # type: ignore[arg-type] + codec = BloscCodec(typesize=typesize, shuffle=shuffle_arg) if shuffle_arg is None: assert codec.shuffle == "bitshuffle" # default shuffle @@ -92,7 +92,7 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int dtype=new_dtype, fill_value=1, prototype=default_buffer_prototype(), - config={}, # type: ignore[arg-type] + config={}, ) evolved_codec = codec.evolve_from_array_spec(array_spec=array_spec) @@ -176,6 +176,23 @@ class LegacyCname(enum.Enum): zstd = "zstd" with pytest.warns(DeprecationWarning, match="enum"): - codec = BloscCodec(cname=LegacyCname.zstd, shuffle=LegacyShuffle.bitshuffle) # type: ignore[arg-type] + codec = BloscCodec(cname=LegacyCname.zstd, shuffle=LegacyShuffle.bitshuffle) assert codec.cname == "zstd" assert codec.shuffle == "bitshuffle" + + +def test_blosc_codec_rejects_unknown_cname() -> None: + with pytest.raises(ValueError, match="cname must be one of"): + BloscCodec(cname="not-a-codec") + + +def test_blosc_codec_rejects_unknown_shuffle() -> None: + with pytest.raises(ValueError, match="shuffle must be one of"): + BloscCodec(shuffle="not-a-shuffle") + + +def test_blosc_enum_attribute_error_for_unknown_member() -> None: + with pytest.raises(AttributeError): + _ = BloscShuffle.not_a_member + with pytest.raises(AttributeError): + _ = BloscCname.not_a_member From 8d4cce86a49630c2ad84bc8eeecf94cb826b63e8 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 00:17:11 +0200 Subject: [PATCH 08/15] test(codecs): use typing.cast instead of type-ignore in blosc tests mypy's per-file (pre-commit) and project-wide views disagree on whether the deliberately-wrong arguments need a type ignore. Using typing.cast keeps both views happy and is more explicit about what each test is asserting. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index de37e2eef3..f798060c49 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -1,6 +1,7 @@ import enum import json import warnings +from typing import cast import numcodecs import numpy as np @@ -10,7 +11,7 @@ import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec -from zarr.codecs.blosc import BloscCname, BloscShuffle, Shuffle +from zarr.codecs.blosc import BloscCname, BloscShuffle, CName, Shuffle from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype @@ -77,7 +78,7 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int else: shuffle_arg = shuffle - codec = BloscCodec(typesize=typesize, shuffle=shuffle_arg) + codec = BloscCodec(typesize=typesize, shuffle=cast(Shuffle | None, shuffle_arg)) if shuffle_arg is None: assert codec.shuffle == "bitshuffle" # default shuffle @@ -92,7 +93,7 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int dtype=new_dtype, fill_value=1, prototype=default_buffer_prototype(), - config={}, + config=cast(ArrayConfig, {}), ) evolved_codec = codec.evolve_from_array_spec(array_spec=array_spec) @@ -176,19 +177,22 @@ class LegacyCname(enum.Enum): zstd = "zstd" with pytest.warns(DeprecationWarning, match="enum"): - codec = BloscCodec(cname=LegacyCname.zstd, shuffle=LegacyShuffle.bitshuffle) + codec = BloscCodec( + cname=cast(BloscCname, LegacyCname.zstd), + shuffle=cast(BloscShuffle, LegacyShuffle.bitshuffle), + ) assert codec.cname == "zstd" assert codec.shuffle == "bitshuffle" def test_blosc_codec_rejects_unknown_cname() -> None: with pytest.raises(ValueError, match="cname must be one of"): - BloscCodec(cname="not-a-codec") + BloscCodec(cname=cast(CName, "not-a-codec")) def test_blosc_codec_rejects_unknown_shuffle() -> None: with pytest.raises(ValueError, match="shuffle must be one of"): - BloscCodec(shuffle="not-a-shuffle") + BloscCodec(shuffle=cast(Shuffle, "not-a-shuffle")) def test_blosc_enum_attribute_error_for_unknown_member() -> None: From 66736af7c8ae70b3fb645d98dd13184ded5e4066 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 12:35:26 +0200 Subject: [PATCH 09/15] refactor(codecs): rename blosc literal aliases to Blosc*Literal Rename Shuffle -> BloscShuffleLiteral and CName -> BloscCnameLiteral. The bare Shuffle name collided with numcodecs.Shuffle re-exported from zarr.codecs, which would have caused mkdocstrings cross-refs in the BloscCodec docstring to resolve to the wrong symbol. The Literal suffix also clearly distinguishes the type alias from the deprecated BloscShuffle / BloscCname enum classes. Update the BloscCodec docstring to reference the new names in the Attributes and Parameters sections (Convention A from cast_value.py), with literal values enumerated in prose. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/zarr/codecs/blosc.py | 54 +++++++++++++++++---------------- tests/test_codecs/test_blosc.py | 14 +++++---- 2 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/zarr/codecs/blosc.py b/src/zarr/codecs/blosc.py index 2ea90d8cb9..6047f0c989 100644 --- a/src/zarr/codecs/blosc.py +++ b/src/zarr/codecs/blosc.py @@ -22,13 +22,13 @@ from zarr.core.array_spec import ArraySpec from zarr.core.buffer import Buffer -Shuffle = Literal["noshuffle", "shuffle", "bitshuffle"] +BloscShuffleLiteral = Literal["noshuffle", "shuffle", "bitshuffle"] """The shuffle values permitted for the blosc codec""" SHUFFLE: Final = ("noshuffle", "shuffle", "bitshuffle") -CName = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] -"""The codec identifiers used in the blosc codec """ +BloscCnameLiteral = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] +"""The codec identifiers used in the blosc codec""" CNAME: Final = ("lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd") @@ -36,7 +36,7 @@ class BloscConfigV2(TypedDict): """Configuration for the V2 Blosc codec""" - cname: CName + cname: BloscCnameLiteral clevel: int shuffle: int blocksize: int @@ -46,9 +46,9 @@ class BloscConfigV2(TypedDict): class BloscConfigV3(TypedDict): """Configuration for the V3 Blosc codec""" - cname: CName + cname: BloscCnameLiteral clevel: int - shuffle: Shuffle + shuffle: BloscShuffleLiteral blocksize: int typesize: int @@ -93,8 +93,8 @@ class BloscShuffle(metaclass=_DeprecatedStrEnumMeta): } @staticmethod - def from_int(num: int) -> Shuffle: - mapping: dict[int, Shuffle] = { + def from_int(num: int) -> BloscShuffleLiteral: + mapping: dict[int, BloscShuffleLiteral] = { 0: "noshuffle", 1: "shuffle", 2: "bitshuffle", @@ -165,13 +165,13 @@ def _coerce_enum_input(value: object, param_name: str) -> object: return value -def _parse_cname(data: object) -> CName: +def _parse_cname(data: object) -> BloscCnameLiteral: if isinstance(data, str) and data in CNAME: return data # type: ignore[return-value] raise ValueError(f"cname must be one of {list(CNAME)!r}. Got {data!r}.") -def _parse_shuffle(data: object) -> Shuffle: +def _parse_shuffle(data: object) -> BloscShuffleLiteral: if isinstance(data, str) and data in SHUFFLE: return data # type: ignore[return-value] raise ValueError(f"shuffle must be one of {list(SHUFFLE)!r}. Got {data!r}.") @@ -192,14 +192,14 @@ class BloscCodec(BytesBytesCodec): Always False for Blosc codec, as compression produces variable-sized output. typesize : int The data type size in bytes used for shuffle filtering. - cname : str - The compression algorithm being used; one of `"lz4"`, `"lz4hc"`, - `"blosclz"`, `"snappy"`, `"zlib"`, or `"zstd"`. + cname : BloscCnameLiteral + The compression algorithm being used; one of "lz4", "lz4hc", + "blosclz", "snappy", "zlib", or "zstd". clevel : int The compression level (0-9). - shuffle : str - The shuffle filter mode; one of `"noshuffle"`, `"shuffle"`, or - `"bitshuffle"`. + shuffle : BloscShuffleLiteral + The shuffle filter mode; one of "noshuffle", "shuffle", or + "bitshuffle". blocksize : int The size of compressed blocks in bytes (0 for automatic). @@ -209,14 +209,16 @@ class BloscCodec(BytesBytesCodec): The data type size in bytes. This affects how the shuffle filter processes the data. If None, defaults to 1 and the attribute is marked as tunable. Default: 1. - cname : {'lz4', 'lz4hc', 'blosclz', 'snappy', 'zlib', 'zstd'}, optional - The compression algorithm to use. Default: `'zstd'`. Passing a - `BloscCname` enum is deprecated. + cname : BloscCnameLiteral, optional + The compression algorithm to use; one of "lz4", "lz4hc", "blosclz", + "snappy", "zlib", or "zstd". Default is "zstd". Passing a `BloscCname` + enum is deprecated. clevel : int, optional The compression level, from 0 (no compression) to 9 (maximum compression). Higher values provide better compression at the cost of speed. Default: 5. - shuffle : {'noshuffle', 'shuffle', 'bitshuffle'}, optional - The shuffle filter to apply before compression: + shuffle : BloscShuffleLiteral or None, optional + The shuffle filter to apply before compression; one of "noshuffle", + "shuffle", or "bitshuffle": - 'noshuffle': No shuffling - 'shuffle': Byte shuffling (better for typesize > 1) @@ -259,18 +261,18 @@ class BloscCodec(BytesBytesCodec): is_fixed_size = False typesize: int - cname: CName + cname: BloscCnameLiteral clevel: int - shuffle: Shuffle + shuffle: BloscShuffleLiteral blocksize: int def __init__( self, *, typesize: int | None = None, - cname: BloscCname | CName = "zstd", + cname: BloscCname | BloscCnameLiteral = "zstd", clevel: int = 5, - shuffle: BloscShuffle | Shuffle | None = None, + shuffle: BloscShuffle | BloscShuffleLiteral | None = None, blocksize: int = 0, ) -> None: object.__setattr__(self, "_tunable_attrs", set()) @@ -339,7 +341,7 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self: @cached_property def _blosc_codec(self) -> Blosc: - map_shuffle_str_to_int: dict[Shuffle, int] = { + map_shuffle_str_to_int: dict[BloscShuffleLiteral, int] = { "noshuffle": 0, "shuffle": 1, "bitshuffle": 2, diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index f798060c49..833de0bf97 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -11,7 +11,7 @@ import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec -from zarr.codecs.blosc import BloscCname, BloscShuffle, CName, Shuffle +from zarr.codecs.blosc import BloscCname, BloscCnameLiteral, BloscShuffle, BloscShuffleLiteral from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype @@ -66,7 +66,9 @@ async def test_blosc_evolve(dtype: str) -> None: @pytest.mark.parametrize("shuffle", [None, "bitshuffle", "legacy-enum"]) @pytest.mark.parametrize("typesize", [None, 1, 2]) -def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int) -> None: +def test_tunable_attrs_param( + shuffle: None | BloscShuffleLiteral | str, typesize: None | int +) -> None: """ Test that the tunable_attrs parameter is set as expected when creating a BloscCodec. """ @@ -74,11 +76,11 @@ def test_tunable_attrs_param(shuffle: None | Shuffle | str, typesize: None | int # contaminating the BloscCodec construction below with that warning. if shuffle == "legacy-enum": with pytest.warns(DeprecationWarning, match="BloscShuffle.shuffle"): - shuffle_arg: None | Shuffle | str = BloscShuffle.shuffle + shuffle_arg: None | BloscShuffleLiteral | str = BloscShuffle.shuffle else: shuffle_arg = shuffle - codec = BloscCodec(typesize=typesize, shuffle=cast(Shuffle | None, shuffle_arg)) + codec = BloscCodec(typesize=typesize, shuffle=cast(BloscShuffleLiteral | None, shuffle_arg)) if shuffle_arg is None: assert codec.shuffle == "bitshuffle" # default shuffle @@ -187,12 +189,12 @@ class LegacyCname(enum.Enum): def test_blosc_codec_rejects_unknown_cname() -> None: with pytest.raises(ValueError, match="cname must be one of"): - BloscCodec(cname=cast(CName, "not-a-codec")) + BloscCodec(cname=cast(BloscCnameLiteral, "not-a-codec")) def test_blosc_codec_rejects_unknown_shuffle() -> None: with pytest.raises(ValueError, match="shuffle must be one of"): - BloscCodec(shuffle=cast(Shuffle, "not-a-shuffle")) + BloscCodec(shuffle=cast(BloscShuffleLiteral, "not-a-shuffle")) def test_blosc_enum_attribute_error_for_unknown_member() -> None: From acfa7a45afc86832434fde55a013b15d38462f2e Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 12:44:56 +0200 Subject: [PATCH 10/15] chore: rename blosc constants --- src/zarr/codecs/blosc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/zarr/codecs/blosc.py b/src/zarr/codecs/blosc.py index 6047f0c989..8a20282060 100644 --- a/src/zarr/codecs/blosc.py +++ b/src/zarr/codecs/blosc.py @@ -25,12 +25,12 @@ BloscShuffleLiteral = Literal["noshuffle", "shuffle", "bitshuffle"] """The shuffle values permitted for the blosc codec""" -SHUFFLE: Final = ("noshuffle", "shuffle", "bitshuffle") +BLOSC_SHUFFLE: Final = ("noshuffle", "shuffle", "bitshuffle") BloscCnameLiteral = Literal["lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd"] """The codec identifiers used in the blosc codec""" -CNAME: Final = ("lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd") +BLOSC_CNAME: Final = ("lz4", "lz4hc", "blosclz", "snappy", "zlib", "zstd") class BloscConfigV2(TypedDict): @@ -166,15 +166,15 @@ def _coerce_enum_input(value: object, param_name: str) -> object: def _parse_cname(data: object) -> BloscCnameLiteral: - if isinstance(data, str) and data in CNAME: + if isinstance(data, str) and data in BLOSC_CNAME: return data # type: ignore[return-value] - raise ValueError(f"cname must be one of {list(CNAME)!r}. Got {data!r}.") + raise ValueError(f"cname must be one of {list(BLOSC_CNAME)!r}. Got {data!r}.") def _parse_shuffle(data: object) -> BloscShuffleLiteral: - if isinstance(data, str) and data in SHUFFLE: + if isinstance(data, str) and data in BLOSC_SHUFFLE: return data # type: ignore[return-value] - raise ValueError(f"shuffle must be one of {list(SHUFFLE)!r}. Got {data!r}.") + raise ValueError(f"shuffle must be one of {list(BLOSC_SHUFFLE)!r}. Got {data!r}.") @dataclass(frozen=True) From 9eebe01c8c2d2d442593a02cd75a13b92582c2d8 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 12:50:43 +0200 Subject: [PATCH 11/15] test(codecs): address review feedback on blosc deprecation tests - Parametrize the BloscShuffle / BloscCname member-access warning tests into a single test_blosc_enum_member_access_warns. - Parametrize the cname / shuffle reject-unknown tests into a single test_blosc_codec_rejects_unknown driven by **kwargs. - Parametrize the AttributeError-on-unknown-member tests into a single test_blosc_enum_attribute_error_for_unknown_member. - Add a docstring to every new test explaining what behavior it verifies, so reviewers don't have to read the body to understand the intent. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 73 ++++++++++++++++++++++----------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index 833de0bf97..4b6d7d751e 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -1,7 +1,7 @@ import enum import json import warnings -from typing import cast +from typing import Any, cast import numcodecs import numpy as np @@ -11,7 +11,7 @@ import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec -from zarr.codecs.blosc import BloscCname, BloscCnameLiteral, BloscShuffle, BloscShuffleLiteral +from zarr.codecs.blosc import BloscCname, BloscShuffle, BloscShuffleLiteral from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype @@ -150,19 +150,30 @@ def test_blosc_codec_sync_roundtrip() -> None: np.testing.assert_array_equal(arr, result) -def test_blosc_shuffle_member_access_warns() -> None: - with pytest.warns(DeprecationWarning, match="BloscShuffle.shuffle"): - value = BloscShuffle.shuffle - assert value == "shuffle" - - -def test_blosc_cname_member_access_warns() -> None: - with pytest.warns(DeprecationWarning, match="BloscCname.zstd"): - value = BloscCname.zstd - assert value == "zstd" +@pytest.mark.parametrize( + ("enum_cls", "member", "expected"), + [ + (BloscShuffle, "shuffle", "shuffle"), + (BloscCname, "zstd", "zstd"), + ], +) +def test_blosc_enum_member_access_warns(enum_cls: type, member: str, expected: str) -> None: + """ + Accessing a member on the deprecated BloscShuffle / BloscCname classes + emits a DeprecationWarning and resolves to the equivalent literal string. + """ + match = f"{enum_cls.__name__}.{member}" + with pytest.warns(DeprecationWarning, match=match): + value = getattr(enum_cls, member) + assert value == expected def test_blosc_enum_classes_import_silently() -> None: + """ + Importing the deprecated enum classes by name must not emit a warning; + only member access does. This guards against the blosc module accidentally + triggering its own deprecation warnings when it (or zarr) is imported. + """ with warnings.catch_warnings(): warnings.simplefilter("error") from zarr.codecs.blosc import BloscCname as _BloscCname # noqa: F401 @@ -170,7 +181,12 @@ def test_blosc_enum_classes_import_silently() -> None: def test_blosc_codec_init_with_enum_instance_warns() -> None: - """A real enum.Enum instance must trigger the init-level deprecation warning.""" + """ + Passing a real `enum.Enum` instance to BloscCodec.__init__ (e.g. an + instance materialized before the deprecation shim was introduced) must + trigger the init-level deprecation warning and still normalize the value + to the corresponding literal string. + """ class LegacyShuffle(enum.Enum): bitshuffle = "bitshuffle" @@ -187,18 +203,25 @@ class LegacyCname(enum.Enum): assert codec.shuffle == "bitshuffle" -def test_blosc_codec_rejects_unknown_cname() -> None: - with pytest.raises(ValueError, match="cname must be one of"): - BloscCodec(cname=cast(BloscCnameLiteral, "not-a-codec")) - - -def test_blosc_codec_rejects_unknown_shuffle() -> None: - with pytest.raises(ValueError, match="shuffle must be one of"): - BloscCodec(shuffle=cast(BloscShuffleLiteral, "not-a-shuffle")) +@pytest.mark.parametrize("param", ["cname", "shuffle"]) +def test_blosc_codec_rejects_unknown(param: str) -> None: + """ + BloscCodec.__init__ raises ValueError when given a string outside the + allowed set for `cname` or `shuffle`, and the error message names the + offending parameter. + """ + kwargs: dict[str, Any] = {param: f"not-a-{param}"} + with pytest.raises(ValueError, match=f"{param} must be one of"): + BloscCodec(**kwargs) -def test_blosc_enum_attribute_error_for_unknown_member() -> None: - with pytest.raises(AttributeError): - _ = BloscShuffle.not_a_member +@pytest.mark.parametrize("enum_cls", [BloscShuffle, BloscCname]) +def test_blosc_enum_attribute_error_for_unknown_member(enum_cls: type) -> None: + """ + Attribute access for a name that is not a known member of the deprecated + enum classes falls through to AttributeError, matching the behavior of a + regular class. + """ + unknown_name = "not_a_member" with pytest.raises(AttributeError): - _ = BloscCname.not_a_member + getattr(enum_cls, unknown_name) From 33a2023d9666b873f24d7a92b52dae8a996fe640 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 13:24:07 +0200 Subject: [PATCH 12/15] test(codecs): parametrize blosc cname/shuffle coverage and JSON roundtrip Backfill missing coverage: previously every test in the blosc suite used only "lz4" or "zstd" for cname and only "bitshuffle" or "shuffle" for shuffle. Add four parametrized tests driven by BLOSC_CNAME / BLOSC_SHUFFLE: - accepts_all_cnames / accepts_all_shuffles: every value in the runtime tuple is accepted by BloscCodec and round-trips on the stored attribute. Catches drift between the BloscCnameLiteral / BloscShuffleLiteral type aliases and their runtime BLOSC_* counterparts. - json_roundtrip_all_cnames / json_roundtrip_all_shuffles: BloscCodec to_dict / from_dict preserves every value. Codec fields are fully specified so the test doesn't trip over tunable-attribute state, which is not part of the JSON form. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 63 ++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index 4b6d7d751e..3fbc003c30 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -11,7 +11,14 @@ import zarr from zarr.abc.codec import SupportsSyncCodec from zarr.codecs import BloscCodec -from zarr.codecs.blosc import BloscCname, BloscShuffle, BloscShuffleLiteral +from zarr.codecs.blosc import ( + BLOSC_CNAME, + BLOSC_SHUFFLE, + BloscCname, + BloscCnameLiteral, + BloscShuffle, + BloscShuffleLiteral, +) from zarr.core.array_spec import ArrayConfig, ArraySpec from zarr.core.buffer import default_buffer_prototype from zarr.core.dtype import UInt16, get_data_type_from_native_dtype @@ -150,6 +157,60 @@ def test_blosc_codec_sync_roundtrip() -> None: np.testing.assert_array_equal(arr, result) +@pytest.mark.parametrize("cname", BLOSC_CNAME) +def test_blosc_codec_accepts_all_cnames(cname: BloscCnameLiteral) -> None: + """ + Every compressor name in BLOSC_CNAME is accepted by BloscCodec and round-trips + to the same value on the stored attribute. Adding a new value to the + BloscCnameLiteral type alias without also adding it to BLOSC_CNAME (or vice + versa) is caught here. + """ + codec = BloscCodec(cname=cname) + assert codec.cname == cname + + +@pytest.mark.parametrize("shuffle", BLOSC_SHUFFLE) +def test_blosc_codec_accepts_all_shuffles(shuffle: BloscShuffleLiteral) -> None: + """ + Every shuffle mode in BLOSC_SHUFFLE is accepted by BloscCodec and round-trips + to the same value on the stored attribute. Adding a new value to the + BloscShuffleLiteral type alias without also adding it to BLOSC_SHUFFLE (or + vice versa) is caught here. + """ + codec = BloscCodec(shuffle=shuffle) + assert codec.shuffle == shuffle + + +@pytest.mark.parametrize("cname", BLOSC_CNAME) +def test_blosc_codec_json_roundtrip_all_cnames(cname: BloscCnameLiteral) -> None: + """ + JSON serialization (to_dict / from_dict) preserves every cname. Guards + against drift in the codec's V3 JSON form for any compressor option. + + The non-cname fields are fully specified so the codec has no tunable + attributes; tunability is not part of the JSON form and would otherwise + cause spurious round-trip mismatches. + """ + codec = BloscCodec(typesize=1, cname=cname, clevel=5, shuffle="shuffle", blocksize=0) + restored = BloscCodec.from_dict(codec.to_dict()) + assert restored == codec + + +@pytest.mark.parametrize("shuffle", BLOSC_SHUFFLE) +def test_blosc_codec_json_roundtrip_all_shuffles(shuffle: BloscShuffleLiteral) -> None: + """ + JSON serialization (to_dict / from_dict) preserves every shuffle mode. + Guards against drift in the codec's V3 JSON form for any shuffle option. + + The non-shuffle fields are fully specified so the codec has no tunable + attributes; tunability is not part of the JSON form and would otherwise + cause spurious round-trip mismatches. + """ + codec = BloscCodec(typesize=1, cname="zstd", clevel=5, shuffle=shuffle, blocksize=0) + restored = BloscCodec.from_dict(codec.to_dict()) + assert restored == codec + + @pytest.mark.parametrize( ("enum_cls", "member", "expected"), [ From 6551a918afbcb4e76a5e16412d0f53f24b31840b Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 17:22:45 +0200 Subject: [PATCH 13/15] test(codecs): collapse blosc JSON roundtrip tests into one parametrize Replace the cname/shuffle JSON-roundtrip pair with a single parametrized test driven by [("cname", v) for v in BLOSC_CNAME] + [("shuffle", v) for v in BLOSC_SHUFFLE]. They asserted the same property (to_dict / from_dict preserves every literal) on two independent axes, so a single test covers both with clear per-case IDs (e.g. cname-lz4, shuffle-bitshuffle). Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 39 +++++++++++++++------------------ 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index 3fbc003c30..d03b23dba6 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -181,32 +181,29 @@ def test_blosc_codec_accepts_all_shuffles(shuffle: BloscShuffleLiteral) -> None: assert codec.shuffle == shuffle -@pytest.mark.parametrize("cname", BLOSC_CNAME) -def test_blosc_codec_json_roundtrip_all_cnames(cname: BloscCnameLiteral) -> None: - """ - JSON serialization (to_dict / from_dict) preserves every cname. Guards - against drift in the codec's V3 JSON form for any compressor option. - - The non-cname fields are fully specified so the codec has no tunable - attributes; tunability is not part of the JSON form and would otherwise - cause spurious round-trip mismatches. - """ - codec = BloscCodec(typesize=1, cname=cname, clevel=5, shuffle="shuffle", blocksize=0) - restored = BloscCodec.from_dict(codec.to_dict()) - assert restored == codec - - -@pytest.mark.parametrize("shuffle", BLOSC_SHUFFLE) -def test_blosc_codec_json_roundtrip_all_shuffles(shuffle: BloscShuffleLiteral) -> None: +@pytest.mark.parametrize( + ("param", "value"), + [("cname", v) for v in BLOSC_CNAME] + [("shuffle", v) for v in BLOSC_SHUFFLE], +) +def test_blosc_codec_json_roundtrip(param: str, value: str) -> None: """ - JSON serialization (to_dict / from_dict) preserves every shuffle mode. - Guards against drift in the codec's V3 JSON form for any shuffle option. + JSON serialization (to_dict / from_dict) preserves every value in + BLOSC_CNAME and BLOSC_SHUFFLE. Guards against drift in the codec's V3 + JSON form for any compressor or shuffle option. - The non-shuffle fields are fully specified so the codec has no tunable + The non-varied fields are fully specified so the codec has no tunable attributes; tunability is not part of the JSON form and would otherwise cause spurious round-trip mismatches. """ - codec = BloscCodec(typesize=1, cname="zstd", clevel=5, shuffle=shuffle, blocksize=0) + kwargs: dict[str, Any] = { + "typesize": 1, + "cname": "zstd", + "clevel": 5, + "shuffle": "shuffle", + "blocksize": 0, + param: value, + } + codec = BloscCodec(**kwargs) restored = BloscCodec.from_dict(codec.to_dict()) assert restored == codec From 0d9c38e6907e8582fc1e322107f6d449f4e3ae41 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Mon, 11 May 2026 17:32:01 +0200 Subject: [PATCH 14/15] test(codecs): cross-product blosc JSON roundtrip over cname x shuffle Stacked parametrize over BLOSC_CNAME and BLOSC_SHUFFLE so the JSON roundtrip exercises every (cname, shuffle) pair (18 cases instead of 9 in a disjoint union). Drops the **kwargs/dict[str, Any] indirection the disjoint form needed, since the cross-product form passes typed arguments directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_codecs/test_blosc.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/tests/test_codecs/test_blosc.py b/tests/test_codecs/test_blosc.py index d03b23dba6..f5f13f4d05 100644 --- a/tests/test_codecs/test_blosc.py +++ b/tests/test_codecs/test_blosc.py @@ -181,29 +181,19 @@ def test_blosc_codec_accepts_all_shuffles(shuffle: BloscShuffleLiteral) -> None: assert codec.shuffle == shuffle -@pytest.mark.parametrize( - ("param", "value"), - [("cname", v) for v in BLOSC_CNAME] + [("shuffle", v) for v in BLOSC_SHUFFLE], -) -def test_blosc_codec_json_roundtrip(param: str, value: str) -> None: +@pytest.mark.parametrize("shuffle", BLOSC_SHUFFLE) +@pytest.mark.parametrize("cname", BLOSC_CNAME) +def test_blosc_codec_json_roundtrip(cname: BloscCnameLiteral, shuffle: BloscShuffleLiteral) -> None: """ - JSON serialization (to_dict / from_dict) preserves every value in - BLOSC_CNAME and BLOSC_SHUFFLE. Guards against drift in the codec's V3 - JSON form for any compressor or shuffle option. + JSON serialization (to_dict / from_dict) preserves every (cname, shuffle) + pair drawn from BLOSC_CNAME x BLOSC_SHUFFLE. Guards against drift in the + codec's V3 JSON form for any combination of compressor and shuffle option. The non-varied fields are fully specified so the codec has no tunable attributes; tunability is not part of the JSON form and would otherwise cause spurious round-trip mismatches. """ - kwargs: dict[str, Any] = { - "typesize": 1, - "cname": "zstd", - "clevel": 5, - "shuffle": "shuffle", - "blocksize": 0, - param: value, - } - codec = BloscCodec(**kwargs) + codec = BloscCodec(typesize=1, cname=cname, clevel=5, shuffle=shuffle, blocksize=0) restored = BloscCodec.from_dict(codec.to_dict()) assert restored == codec From e90d2ba01134d474f212eef268b0700dd1bb8369 Mon Sep 17 00:00:00 2001 From: Davis Bennett Date: Mon, 11 May 2026 17:51:19 +0200 Subject: [PATCH 15/15] Rename 0000.removal.md to 3963.removal.md --- changes/{0000.removal.md => 3963.removal.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{0000.removal.md => 3963.removal.md} (100%) diff --git a/changes/0000.removal.md b/changes/3963.removal.md similarity index 100% rename from changes/0000.removal.md rename to changes/3963.removal.md