From 23c4aa0a554c9e552cbb08cf1465fd17ba2d4c1b Mon Sep 17 00:00:00 2001 From: victorwon2001 <192616110+victorwon2001@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:02:58 +0900 Subject: [PATCH] feat: align Zarr array metadata class names --- src/zarr/core/metadata/__init__.py | 8 +++++--- src/zarr/core/metadata/v2.py | 11 ++++++---- src/zarr/core/metadata/v3.py | 13 +++++++----- tests/test_metadata/test_v2.py | 30 +++++++++++++++++++++++++++- tests/test_metadata/test_v3.py | 32 ++++++++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/zarr/core/metadata/__init__.py b/src/zarr/core/metadata/__init__.py index cacfc933b5..db31e9c153 100644 --- a/src/zarr/core/metadata/__init__.py +++ b/src/zarr/core/metadata/__init__.py @@ -1,13 +1,15 @@ -from .v2 import ArrayV2Metadata, ArrayV2MetadataDict -from .v3 import ArrayMetadataJSON_V3, ArrayV3Metadata +from .v2 import ArrayMetadataV2, ArrayV2Metadata, ArrayV2MetadataDict +from .v3 import ArrayMetadataJSON_V3, ArrayMetadataV3, ArrayV3Metadata -ArrayMetadata = ArrayV2Metadata | ArrayV3Metadata +ArrayMetadata = ArrayMetadataV2 | ArrayMetadataV3 type ArrayMetadataDict = ArrayV2MetadataDict | ArrayMetadataJSON_V3 __all__ = [ "ArrayMetadata", "ArrayMetadataDict", "ArrayMetadataJSON_V3", + "ArrayMetadataV2", + "ArrayMetadataV3", "ArrayV2Metadata", "ArrayV2MetadataDict", "ArrayV3Metadata", diff --git a/src/zarr/core/metadata/v2.py b/src/zarr/core/metadata/v2.py index 136267fc58..24fc8a69b4 100644 --- a/src/zarr/core/metadata/v2.py +++ b/src/zarr/core/metadata/v2.py @@ -62,7 +62,7 @@ class ArrayV2MetadataDict(TypedDict): @dataclass(frozen=True, kw_only=True) -class ArrayV2Metadata(Metadata): +class ArrayMetadataV2(Metadata): shape: tuple[int, ...] chunks: tuple[int, ...] dtype: ZDType[TBaseDType, TBaseScalar] @@ -152,7 +152,7 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]: } @classmethod - def from_dict(cls, data: dict[str, Any], *, path: str | None = None) -> ArrayV2Metadata: + def from_dict(cls, data: dict[str, Any], *, path: str | None = None) -> ArrayMetadataV2: """Read a stored `.zarray` document (with its attributes). An invalid document that `zarr.core.metadata.repair` can read is read as repaired; a reading the user must act on warns, naming the array at `path`.""" @@ -187,7 +187,7 @@ def from_dict(cls, data: dict[str, Any], *, path: str | None = None) -> ArrayV2M _data["fill_value"] = fill_value # zarr v2 allowed arbitrary keys here. - # We don't want the ArrayV2Metadata constructor to fail just because someone put an + # We don't want the ArrayMetadataV2 constructor to fail just because someone put an # extra key in the metadata. expected = {x.name for x in fields(cls)} expected |= {"dtype", "chunks"} @@ -248,7 +248,7 @@ def __eq__(self, other: object) -> bool: # The default dataclass __eq__ compares fields directly, which is wrong for a NaN # fill_value: NaN != NaN under IEEE 754. Comparing the JSON-serialized form instead # treats matching NaN (and inf) fill values as equal. See issue #2929. - if not isinstance(other, ArrayV2Metadata): + if not isinstance(other, ArrayMetadataV2): return NotImplemented return self.to_dict() == other.to_dict() @@ -279,6 +279,9 @@ def update_attributes(self, attributes: dict[str, JSON]) -> Self: return replace(self, attributes=attributes) +ArrayV2Metadata = ArrayMetadataV2 + + def parse_dtype(data: npt.DTypeLike) -> np.dtype[Any]: if isinstance(data, list): # this is a valid _VoidDTypeLike check data = [tuple(d) for d in data] diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py index 2d5d169e6b..03bc5f77bf 100644 --- a/src/zarr/core/metadata/v3.py +++ b/src/zarr/core/metadata/v3.py @@ -270,7 +270,7 @@ class RegularChunkGridMetadata(Metadata): """Metadata-only description of a regular chunk grid. Stores just the chunk shape — no array extent, no runtime logic. - This is what lives on ``ArrayV3Metadata.chunk_grid``. + This is what lives on ``ArrayMetadataV3.chunk_grid``. """ chunk_shape: tuple[int, ...] @@ -400,13 +400,13 @@ def _check_rectilinear_chunks_enabled() -> None: ) -def check_storable(metadata: ArrayV3Metadata) -> None: +def check_storable(metadata: ArrayMetadataV3) -> None: """Raise if `metadata` may not be stored: a rectilinear chunk grid requires the rectilinear chunks flag. `zarr.core.metadata.io.encode_documents` names the node in the error. Every serialization of array metadata for a store calls this, before the store is - touched: `ArrayV3Metadata.to_buffer_dict` and, for the arrays in a group's + touched: `ArrayMetadataV3.to_buffer_dict` and, for the arrays in a group's consolidated metadata, `GroupMetadata.to_buffer_dict`. """ if isinstance(metadata.chunk_grid, RectilinearChunkGridMetadata): @@ -506,7 +506,7 @@ class ArrayMetadataJSON_V3(TypedDict, extra_items=AllowedExtraField): # type: i @dataclass(frozen=True, kw_only=True) -class ArrayV3Metadata(Metadata): +class ArrayMetadataV3(Metadata): shape: tuple[int, ...] data_type: ZDType[TBaseDType, TBaseScalar] chunk_grid: ChunkGridMetadata @@ -756,7 +756,7 @@ def __eq__(self, other: object) -> bool: # The default dataclass __eq__ compares fields directly, which is wrong for a NaN # fill_value: NaN != NaN under IEEE 754. Comparing the JSON-serialized form instead # treats matching NaN (and inf) fill values as equal. See issue #2929. - if not isinstance(other, ArrayV3Metadata): + if not isinstance(other, ArrayMetadataV3): return NotImplemented return self.to_dict() == other.to_dict() @@ -773,3 +773,6 @@ def update_shape(self, shape: tuple[int, ...]) -> Self: def update_attributes(self, attributes: dict[str, JSON]) -> Self: return replace(self, attributes=attributes) + + +ArrayV3Metadata = ArrayMetadataV3 diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index 4f957f0f19..dc27b58459 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -1,19 +1,22 @@ from __future__ import annotations import json +import pickle from typing import TYPE_CHECKING, Literal import numpy as np import pytest import zarr.api.asynchronous +import zarr.core.metadata as metadata_exports +import zarr.core.metadata.v2 as metadata_v2 import zarr.storage from zarr.core.buffer import cpu from zarr.core.buffer.core import default_buffer_prototype from zarr.core.dtype.npy.float import Float32, Float64 from zarr.core.dtype.npy.int import Int16 from zarr.core.group import ConsolidatedMetadata, GroupMetadata -from zarr.core.metadata import ArrayV2Metadata +from zarr.core.metadata import ArrayMetadata, ArrayMetadataV2, ArrayV2Metadata from zarr.core.metadata.v2 import parse_zarr_format from zarr.errors import ZarrUserWarning @@ -26,6 +29,31 @@ from zarr.core.dtype.common import DTypeName_V2 +def test_array_metadata_v2_names_are_compatible() -> None: + assert metadata_v2.ArrayMetadataV2 is ArrayMetadataV2 + assert metadata_v2.ArrayV2Metadata is ArrayV2Metadata is ArrayMetadataV2 + assert "ArrayMetadataV2" in metadata_exports.__all__ + assert "ArrayV2Metadata" in metadata_exports.__all__ + assert ArrayMetadataV2.__name__ == "ArrayMetadataV2" + + metadata = ArrayMetadataV2(shape=(8,), chunks=(4,), dtype=Float64(), fill_value=0.0, order="C") + assert type(metadata) is ArrayMetadataV2 + assert isinstance(metadata, (ArrayV2Metadata, ArrayMetadata)) + match metadata: + case ArrayV2Metadata(): + pass + case _: + pytest.fail("Legacy V2 class pattern no longer matches") + + restored = ArrayV2Metadata.from_dict(metadata.to_dict()) + assert type(restored) is ArrayMetadataV2 + assert restored == metadata + assert pickle.loads(pickle.dumps(metadata)) == metadata + assert pickle.loads(b"czarr.core.metadata.v2\nArrayV2Metadata\n.") is ArrayMetadataV2 + assert metadata.update_shape((12,)).shape == (12,) + assert metadata.update_attributes({"key": "value"}).attributes == {"key": "value"} + + def test_parse_zarr_format_valid() -> None: assert parse_zarr_format(2) == 2 diff --git a/tests/test_metadata/test_v3.py b/tests/test_metadata/test_v3.py index 94240c36b9..a573939302 100644 --- a/tests/test_metadata/test_v3.py +++ b/tests/test_metadata/test_v3.py @@ -3,11 +3,14 @@ from __future__ import annotations import json +import pickle import re from typing import TYPE_CHECKING import pytest +import zarr.core.metadata as metadata_exports +import zarr.core.metadata.v3 as metadata_v3 from tests.conftest import Expect, ExpectFail from tests.test_metadata.conftest import minimal_metadata_dict_v3 from zarr.core.buffer import default_buffer_prototype @@ -15,6 +18,7 @@ from zarr.core.config import config from zarr.core.dtype import Float64, UInt8 from zarr.core.group import GroupMetadata, parse_node_type +from zarr.core.metadata import ArrayMetadata, ArrayMetadataV3 from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ( ARRAY_METADATA_KEYS, @@ -37,6 +41,34 @@ from typing import Any +def test_array_metadata_v3_names_are_compatible() -> None: + assert metadata_v3.ArrayMetadataV3 is ArrayMetadataV3 + assert metadata_v3.ArrayV3Metadata is ArrayV3Metadata is ArrayMetadataV3 + assert "ArrayMetadataV3" in metadata_exports.__all__ + assert "ArrayV3Metadata" in metadata_exports.__all__ + assert ArrayMetadataV3.__name__ == "ArrayMetadataV3" + + metadata = ArrayMetadataV3.from_dict( + minimal_metadata_dict_v3(data_type="float64", fill_value="NaN") # type: ignore[arg-type] + ) + assert type(metadata) is ArrayMetadataV3 + assert isinstance(metadata, (ArrayV3Metadata, ArrayMetadata)) + match metadata: + case ArrayV3Metadata(): + pass + case _: + pytest.fail("Legacy V3 class pattern no longer matches") + + restored = ArrayV3Metadata.from_dict(metadata.to_dict()) + assert type(restored) is ArrayMetadataV3 + assert restored == metadata + assert hash(restored) == hash(metadata) + assert pickle.loads(pickle.dumps(metadata)) == metadata + assert pickle.loads(b"czarr.core.metadata.v3\nArrayV3Metadata\n.") is ArrayMetadataV3 + assert metadata.update_shape((8, 8)).shape == (8, 8) + assert metadata.update_attributes({"key": "value"}).attributes == {"key": "value"} + + # --------------------------------------------------------------------------- # Parsing helpers # ---------------------------------------------------------------------------