diff --git a/changes/4381.bugfix.md b/changes/4381.bugfix.md new file mode 100644 index 0000000000..12d4189400 --- /dev/null +++ b/changes/4381.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