From 2aafe516191fedf87f932c8fb585d18e1d106372 Mon Sep 17 00:00:00 2001 From: guhou-hvi <1322077960@qq.com> Date: Sun, 20 Sep 2026 17:38:35 +0800 Subject: [PATCH 1/2] Fix Zarr v2 uint8 dtype aliases (GH3622) --- changes/+uint8-dtype-aliases.bugfix.md | 1 + src/zarr/core/dtype/npy/int.py | 6 +++++- tests/test_dtype/test_npy/test_int.py | 30 ++++++++++++++++++++++++++ tests/test_metadata/test_v2.py | 23 ++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 changes/+uint8-dtype-aliases.bugfix.md diff --git a/changes/+uint8-dtype-aliases.bugfix.md b/changes/+uint8-dtype-aliases.bugfix.md new file mode 100644 index 0000000000..12d4189400 --- /dev/null +++ b/changes/+uint8-dtype-aliases.bugfix.md @@ -0,0 +1 @@ +Zarr format 2 arrays with a `u1` data type can now be read as unsigned 8-bit integers. Serialized metadata continues to use the canonical `|u1` spelling. diff --git a/src/zarr/core/dtype/npy/int.py b/src/zarr/core/dtype/npy/int.py index c18fd01dd8..c6080dbc6a 100644 --- a/src/zarr/core/dtype/npy/int.py +++ b/src/zarr/core/dtype/npy/int.py @@ -415,7 +415,11 @@ class UInt8(BaseInt[np.dtypes.UInt8DType, np.uint8]): dtype_cls = np.dtypes.UInt8DType _zarr_v3_name: ClassVar[Literal["uint8"]] = "uint8" - _zarr_v2_names: ClassVar[tuple[Literal["|u1"]]] = ("|u1",) + _zarr_v2_names: ClassVar[tuple[Literal["|u1"], Literal["u1"]]] = ( + "|u1", + "u1", + ) @classmethod def from_native_dtype(cls, dtype: TBaseDType) -> Self: diff --git a/tests/test_dtype/test_npy/test_int.py b/tests/test_dtype/test_npy/test_int.py index 9eab053080..bbe2566701 100644 --- a/tests/test_dtype/test_npy/test_int.py +++ b/tests/test_dtype/test_npy/test_int.py @@ -1,9 +1,12 @@ from __future__ import annotations import numpy as np +import pytest from tests.test_dtype.test_wrapper import BaseTestZDType +from zarr.core.dtype import get_data_type_from_json from zarr.core.dtype.npy.int import Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 +from zarr.errors import DataTypeValidationError class TestInt8(BaseTestZDType): @@ -329,3 +332,30 @@ def test_string_integer_from_json_scalar() -> None: # Test that it works for v2 format too result = dtype_instance.from_json_scalar("123", zarr_format=2) assert result == np.int32(123) + + +@pytest.mark.parametrize("name", ["|u1", "u1"]) +def test_uint8_v2_aliases(name: str) -> None: + data = {"name": name, "object_codec_id": None} + dtype = UInt8.from_json(data, zarr_format=2) + assert dtype == UInt8() + assert get_data_type_from_json(data, zarr_format=2) == dtype + assert dtype.to_native_dtype() == np.dtype("uint8") + serialized = dtype.to_json(zarr_format=2) + assert serialized["name"] == "|u1" + assert serialized["object_codec_id"] is None + assert dtype.to_json(zarr_format=3) == "uint8" + with pytest.raises(DataTypeValidationError): + UInt8.from_json(name, zarr_format=3) + + +@pytest.mark.parametrize("name", ["|u1", "u1"]) +def test_uint8_v2_aliases_reject_object_codec(name: str) -> None: + with pytest.raises(DataTypeValidationError): + UInt8.from_json({"name": name, "object_codec_id": "vlen-utf8"}, zarr_format=2) + + +@pytest.mark.parametrize("name", ["|i1", "u2", "uint8", "invalid"]) +def test_uint8_v2_rejects_other_types(name: str) -> None: + with pytest.raises(DataTypeValidationError): + UInt8.from_json({"name": name, "object_codec_id": None}, zarr_format=2) diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index 1358f458d6..322c62c3e7 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -399,3 +399,26 @@ def test_structured_dtype_fill_value_serialization( root_group = zarr.open_group(group_path, mode="r") observed = root_group.metadata.consolidated_metadata.metadata["structured_dtype"].fill_value # type: ignore[union-attr] assert observed == fill_value + + +@pytest.mark.parametrize("dtype", ["|u1", "u1"]) +def test_open_uint8_dtype_aliases(tmp_path: Path, dtype: str) -> None: + metadata = { + "zarr_format": 2, + "shape": [3], + "chunks": [3], + "dtype": dtype, + "compressor": None, + "fill_value": 0, + "order": "C", + "filters": None, + } + metadata_path = tmp_path / ".zarray" + metadata_path.write_text(json.dumps(metadata)) + (tmp_path / "0").write_bytes(bytes([0, 128, 255])) + + array = zarr.open_array(tmp_path, mode="r") + assert array.dtype == np.dtype("uint8") + np.testing.assert_array_equal(array[:], np.array([0, 128, 255], dtype=np.uint8)) + assert array.metadata.to_dict()["dtype"] == "|u1" + assert json.loads(metadata_path.read_text()) == metadata From a2787b65dc70d57c82aaacc01784e90c2ab26103 Mon Sep 17 00:00:00 2001 From: guhou-hvi <1322077960@qq.com> Date: Sun, 20 Sep 2026 18:33:44 +0800 Subject: [PATCH 2/2] docs: associate changelog with PR 4381 --- changes/{+uint8-dtype-aliases.bugfix.md => 4381.bugfix.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{+uint8-dtype-aliases.bugfix.md => 4381.bugfix.md} (100%) diff --git a/changes/+uint8-dtype-aliases.bugfix.md b/changes/4381.bugfix.md similarity index 100% rename from changes/+uint8-dtype-aliases.bugfix.md rename to changes/4381.bugfix.md