Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a57f745
refactor(codecs): extract shared enum-deprecation helpers
d-v-b May 13, 2026
778519c
refactor(codecs): use shared enum-deprecation helpers in blosc
d-v-b May 13, 2026
8e3244d
feat(codecs): deprecate Endian enum
d-v-b May 13, 2026
a90f4dc
fixup(codecs): restore spec form for Endian deprecation
d-v-b May 13, 2026
c4f7cb1
refactor(buffer): widen NDBuffer.byteorder to EndianLiteral
d-v-b May 13, 2026
a5ceda1
chore(codecs): drop now-unused type-ignore in BytesCodec._encode_sync
d-v-b May 13, 2026
e3895e1
feat(codecs): deprecate ShardingCodecIndexLocation enum
d-v-b May 13, 2026
686bcd4
test: rewrite sharding-test parametrize decorators to literal strings
d-v-b May 13, 2026
f7eca85
refactor(core): use IndexLocationLiteral in array module
d-v-b May 13, 2026
7b5c9f6
test: use literal strings for sharding/endian in conftest + test_info
d-v-b May 13, 2026
3ff470e
chore(changes): add changelog entry for Endian + sharding enum deprec…
d-v-b May 13, 2026
695fb58
chore(changes): document NDBuffer.byteorder and default_system_endian…
d-v-b May 13, 2026
123add0
test(codecs): tighten deprecation-warning patterns and cover legacy i…
d-v-b May 13, 2026
b2633c1
refactor: drop _parse_index_location cross-module imports
d-v-b May 13, 2026
3094d99
test(codecs): cover bytes evolve_from_array_spec and init_array dict-…
d-v-b May 13, 2026
2908cb8
refactor: rename IndexLocationLiteral to IndexLocation
d-v-b May 13, 2026
f55f792
Merge branch 'main' into deprecate-more-enums
d-v-b May 15, 2026
82cbdce
Merge branch 'main' into deprecate-more-enums
d-v-b May 18, 2026
03443fb
Merge origin/deprecate-more-enums into deprecate-more-enums
d-v-b May 18, 2026
5a6aaeb
Merge branch 'deprecate-more-enums' of https://github.com/d-v-b/zarr-…
d-v-b May 18, 2026
d264cc7
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b May 18, 2026
8da1333
chore(deps): bump the actions group across 1 directory with 8 updates…
dependabot[bot] May 31, 2026
659c734
Merge branch 'main' of https://github.com/d-v-b/zarr-python
d-v-b Jun 6, 2026
51c994b
Merge branch 'main' of https://github.com/zarr-developers/zarr-python
d-v-b Jun 6, 2026
117b7ba
Merge branch 'main' of github.com:d-v-b/zarr-python
d-v-b Jun 12, 2026
d4de75d
Merge branch 'main' of github.com:zarr-developers/zarr-python
d-v-b Jun 12, 2026
86dabd5
Merge branch 'main' of github.com:zarr-developers/zarr-python
d-v-b Jun 12, 2026
34a9c78
Merge branch 'deprecate-more-enums' of github.com:d-v-b/zarr-python i…
d-v-b Jun 12, 2026
47b4d81
docs: clarify that _coerce_enum_input only fires for foreign enum ins…
d-v-b Jun 12, 2026
3f54f3e
Merge branch 'main' of github.com:zarr-developers/zarr-python into de…
d-v-b Jun 12, 2026
376a684
docs: rename changelog
d-v-b Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions changes/3968.removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
The ``Endian`` (``zarr.codecs.bytes.Endian``) and ``ShardingCodecIndexLocation``
(``zarr.codecs.ShardingCodecIndexLocation``) enums are now deprecated. Pass the
equivalent literal string instead (e.g. ``"little"`` / ``"big"``, ``"start"`` /
``"end"``). The enum classes remain importable but emit ``DeprecationWarning``
on member access, and will be removed in a future release. ``BytesCodec.endian``
and ``ShardingCodec.index_location`` are now plain strings rather than enum
members.

Two follow-on changes from this deprecation:

- ``NDBuffer.byteorder`` now returns a literal string (``"little"`` or
``"big"``) rather than an ``Endian`` member. Subclasses overriding this
property should update their return type.
- The module-level binding ``zarr.codecs.bytes.default_system_endian`` was
removed. ``BytesCodec()`` continues to default to ``sys.byteorder``;
external callers that imported ``default_system_endian`` should use
``sys.byteorder`` directly.
59 changes: 59 additions & 0 deletions src/zarr/codecs/_deprecated_enum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Helpers for deprecating string-valued enums in favor of literal strings.

See PR #3963 for context on the deprecation pattern.
"""

from __future__ import annotations

import warnings
from enum import Enum


class _DeprecatedStrEnumMeta(type):
"""
Metaclass for legacy enum-like classes. Accessing a member name on the
class (e.g. `LegacyShim.foo`) emits a `DeprecationWarning` and returns
the equivalent string. Members are declared by setting a `_members`
class attribute mapping each member name to its string value.
"""

_members: dict[str, 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)


def _coerce_enum_input(value: object, param_name: str, codec_name: str) -> object:
"""
If `value` is a real `enum.Enum` instance, emit a deprecation warning
naming `codec_name` and return `value.value`. Otherwise return `value`
unchanged. The third argument lets the warning text name the actual
codec (e.g. `BloscCodec`, `BytesCodec`, `ShardingCodec`).

Note that zarr's own legacy classes (e.g. `ShardingCodecIndexLocation`)
never reach the `Enum` branch here: they no longer inherit from `Enum`,
and member access on them already returns a plain string (with its own
warning) via `_DeprecatedStrEnumMeta`. This branch exists for enum
instances defined *outside* zarr — in particular `str`-mixin enums that
downstream code defined to mirror zarr's old enums, which the old
`parse_enum`-based codepath accepted because they are `str` instances.
Coercing them to `value.value` keeps the stored attribute a plain string
and gives those callers a migration warning.
"""
if isinstance(value, Enum):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I missed was that this line is not called by instantiating a class with a now-deprecated former Enum because nothing actually inherits from Enum anymore. What is this for then? Am I misreading this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is very defensive, to cover a situation where someone defined their own str enum outside of zarr, which would have worked before.

warnings.warn(
f"Passing an enum to {codec_name}(..., {param_name}=...) is deprecated; "
"pass the equivalent literal string instead.",
DeprecationWarning,
stacklevel=3,
)
return value.value
return value
44 changes: 3 additions & 41 deletions src/zarr/codecs/blosc.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
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, ClassVar, Final, Literal, NotRequired, TypedDict

Expand All @@ -12,6 +10,7 @@
from packaging.version import Version

from zarr.abc.codec import BytesBytesCodec
from zarr.codecs._deprecated_enum import _coerce_enum_input, _DeprecatedStrEnumMeta
from zarr.core.buffer.cpu import as_numpy_array_wrapper
from zarr.core.common import JSON, NamedRequiredConfig, parse_named_configuration
from zarr.core.dtype.common import HasItemSize
Expand Down Expand Up @@ -59,27 +58,6 @@ 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`
and returns the equivalent string.
"""

_members: dict[str, 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
Expand Down Expand Up @@ -149,22 +127,6 @@ 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 `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) -> BloscCnameLiteral:
if isinstance(data, str) and data in BLOSC_CNAME:
return data # type: ignore[return-value]
Expand Down Expand Up @@ -285,8 +247,8 @@ def __init__(
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]
cname = _coerce_enum_input(cname, "cname", "BloscCodec") # type: ignore[assignment]
shuffle = _coerce_enum_input(shuffle, "shuffle", "BloscCodec") # type: ignore[assignment]

typesize_parsed = parse_typesize(typesize)
cname_parsed = _parse_cname(cname)
Expand Down
46 changes: 28 additions & 18 deletions src/zarr/codecs/bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import sys
import warnings
from dataclasses import dataclass, replace
from enum import Enum
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar, Final, Literal

from zarr.abc.codec import ArrayBytesCodec
from zarr.codecs._deprecated_enum import _coerce_enum_input, _DeprecatedStrEnumMeta
from zarr.core.buffer import Buffer, NDBuffer
from zarr.core.common import JSON, parse_enum, parse_named_configuration
from zarr.core.common import JSON, parse_named_configuration
from zarr.core.dtype.common import HasEndianness
from zarr.core.dtype.npy.structured import Struct

Expand All @@ -18,16 +18,25 @@
from zarr.core.array_spec import ArraySpec


class Endian(Enum):
EndianLiteral = Literal["little", "big"]
"""Byte order of multi-byte numeric data."""

ENDIAN: Final = ("little", "big")


class Endian(metaclass=_DeprecatedStrEnumMeta):
"""
Enum for endian type used by bytes codec.
Deprecated. Pass a literal string (`"little"` or `"big"`) directly to
`BytesCodec` instead.
"""

big = "big"
little = "little"
_members: ClassVar[dict[str, str]] = {"little": "little", "big": "big"}


default_system_endian = Endian(sys.byteorder)
def _parse_endian(data: object) -> EndianLiteral:
if isinstance(data, str) and data in ENDIAN:
return data # type: ignore[return-value]
raise ValueError(f"endian must be one of {list(ENDIAN)!r}. Got {data!r}.")


@dataclass(frozen=True)
Expand All @@ -36,10 +45,14 @@ class BytesCodec(ArrayBytesCodec):

is_fixed_size = True

endian: Endian | None
endian: EndianLiteral | None

def __init__(self, *, endian: Endian | str | None = default_system_endian) -> None:
endian_parsed = None if endian is None else parse_enum(endian, Endian)
def __init__(self, *, endian: Endian | EndianLiteral | None = sys.byteorder) -> None:
if endian is None:
endian_parsed: EndianLiteral | None = None
else:
coerced = _coerce_enum_input(endian, "endian", "BytesCodec")
endian_parsed = _parse_endian(coerced)

object.__setattr__(self, "endian", endian_parsed)

Expand All @@ -55,7 +68,7 @@ def to_dict(self) -> dict[str, JSON]:
if self.endian is None:
return {"name": "bytes"}
else:
return {"name": "bytes", "configuration": {"endian": self.endian.value}}
return {"name": "bytes", "configuration": {"endian": self.endian}}

def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self:
if isinstance(array_spec.dtype, Struct):
Expand All @@ -67,7 +80,7 @@ def evolve_from_array_spec(self, array_spec: ArraySpec) -> Self:
UserWarning,
stacklevel=2,
)
return replace(self, endian=Endian.little)
return replace(self, endian="little")
else:
if self.endian is not None:
return replace(self, endian=None)
Expand All @@ -85,8 +98,7 @@ def _decode_sync(
chunk_bytes: Buffer,
chunk_spec: ArraySpec,
) -> NDBuffer:
# TODO: remove endianness enum in favor of literal union
endian_str = self.endian.value if self.endian is not None else None
endian_str = self.endian
if isinstance(chunk_spec.dtype, HasEndianness):
dtype = replace(chunk_spec.dtype, endianness=endian_str).to_native_dtype() # type: ignore[call-arg]
else:
Expand Down Expand Up @@ -121,9 +133,7 @@ def _encode_sync(
and self.endian is not None
and self.endian != chunk_array.byteorder
):
# type-ignore is a numpy bug
# see https://github.com/numpy/numpy/issues/26473
new_dtype = chunk_array.dtype.newbyteorder(self.endian.name) # type: ignore[arg-type]
new_dtype = chunk_array.dtype.newbyteorder(self.endian)
chunk_array = chunk_array.astype(new_dtype)

nd_array = chunk_array.as_ndarray_like()
Expand Down
Loading
Loading