From 1b7b883b7c301ade3506c83702c70cfa44ae76cc Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 28 Jul 2025 15:51:45 +0100 Subject: [PATCH 1/5] Parametrize Array with v2/v3 metadata --- src/zarr/api/asynchronous.py | 9 ++-- src/zarr/api/synchronous.py | 33 +++++++------ src/zarr/core/array.py | 32 ++++++------ src/zarr/core/attributes.py | 4 +- src/zarr/core/group.py | 49 ++++++++++--------- src/zarr/core/indexing.py | 19 +++---- src/zarr/core/metadata/__init__.py | 2 +- src/zarr/core/sync_group.py | 10 ++-- src/zarr/testing/strategies.py | 3 +- src/zarr/types.py | 14 ++++++ tests/test_api.py | 5 +- tests/test_api/test_asynchronous.py | 8 ++- tests/test_array.py | 15 +++--- tests/test_attributes.py | 9 ++-- .../test_v2_dtype_regression.py | 10 ++-- 15 files changed, 127 insertions(+), 95 deletions(-) create mode 100644 src/zarr/types.py diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index d3613f7c05..2f6777a74a 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -3,7 +3,7 @@ import asyncio import dataclasses import warnings -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast import numpy as np import numpy.typing as npt @@ -56,9 +56,12 @@ from zarr.core.buffer import NDArrayLikeOrScalar from zarr.core.chunk_key_encodings import ChunkKeyEncoding from zarr.storage import StoreLike + from zarr.types import AnyArray # TODO: this type could use some more thought - ArrayLike = AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | Array | npt.NDArray[Any] + ArrayLike: TypeAlias = ( + AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AnyArray | npt.NDArray[Any] + ) PathLike = str __all__ = [ @@ -568,7 +571,7 @@ async def tree(grp: AsyncGroup, expand: bool | None = None, level: int | None = async def array( - data: npt.ArrayLike | Array, **kwargs: Any + data: npt.ArrayLike | AnyArray, **kwargs: Any ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: """Create an array filled with `data`. diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 1146a6876f..a092c84cbb 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -40,6 +40,7 @@ ) from zarr.core.dtype import ZDTypeLike from zarr.storage import StoreLike + from zarr.types import AnyArray __all__ = [ "array", @@ -168,7 +169,7 @@ def open( path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to async_api.open -) -> Array | Group: +) -> AnyArray | Group: """Open a group or array using file-mode-like semantics. Parameters @@ -365,7 +366,7 @@ def tree(grp: Group, expand: bool | None = None, level: int | None = None) -> An # TODO: add type annotations for kwargs -def array(data: npt.ArrayLike | Array, **kwargs: Any) -> Array: +def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyArray: """Create an array filled with `data`. Parameters @@ -633,7 +634,7 @@ def create( storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, **kwargs: Any, -) -> Array: +) -> AnyArray: """Create an array. Parameters @@ -769,7 +770,7 @@ def create_array( overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, -) -> Array: +) -> AnyArray: """Create an array. This function wraps :func:`zarr.core.array.create_array`. @@ -916,7 +917,7 @@ def create_array( def from_array( store: str | StoreLike, *, - data: Array | npt.ArrayLike, + data: AnyArray | npt.ArrayLike, write_data: bool = True, name: str | None = None, chunks: Literal["auto", "keep"] | tuple[int, ...] = "keep", @@ -933,7 +934,7 @@ def from_array( storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfigLike | None = None, -) -> Array: +) -> AnyArray: """Create an array from an existing array or array-like. Parameters @@ -1127,7 +1128,7 @@ def from_array( # TODO: add type annotations for kwargs -def empty(shape: tuple[int, ...], **kwargs: Any) -> Array: +def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an empty array with the specified shape. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1154,7 +1155,7 @@ def empty(shape: tuple[int, ...], **kwargs: Any) -> Array: # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs -def empty_like(a: ArrayLike, **kwargs: Any) -> Array: +def empty_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an empty array like another array. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1180,7 +1181,7 @@ def empty_like(a: ArrayLike, **kwargs: Any) -> Array: # TODO: add type annotations for kwargs and fill_value -def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> Array: +def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyArray: """Create an array with a default fill value. Parameters @@ -1202,7 +1203,7 @@ def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> Array: # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs -def full_like(a: ArrayLike, **kwargs: Any) -> Array: +def full_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create a filled array like another array. Parameters @@ -1221,7 +1222,7 @@ def full_like(a: ArrayLike, **kwargs: Any) -> Array: # TODO: add type annotations for kwargs -def ones(shape: tuple[int, ...], **kwargs: Any) -> Array: +def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array with a fill value of one. Parameters @@ -1240,7 +1241,7 @@ def ones(shape: tuple[int, ...], **kwargs: Any) -> Array: # TODO: add type annotations for kwargs -def ones_like(a: ArrayLike, **kwargs: Any) -> Array: +def ones_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an array of ones like another array. Parameters @@ -1266,7 +1267,7 @@ def open_array( path: PathLike = "", storage_options: dict[str, Any] | None = None, **kwargs: Any, -) -> Array: +) -> AnyArray: """Open an array using file-mode-like semantics. Parameters @@ -1302,7 +1303,7 @@ def open_array( # TODO: add type annotations for kwargs -def open_like(a: ArrayLike, path: str, **kwargs: Any) -> Array: +def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyArray: """Open a persistent array like another array. Parameters @@ -1323,7 +1324,7 @@ def open_like(a: ArrayLike, path: str, **kwargs: Any) -> Array: # TODO: add type annotations for kwargs -def zeros(shape: tuple[int, ...], **kwargs: Any) -> Array: +def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array with a fill value of zero. Parameters @@ -1342,7 +1343,7 @@ def zeros(shape: tuple[int, ...], **kwargs: Any) -> Array: # TODO: add type annotations for kwargs -def zeros_like(a: ArrayLike, **kwargs: Any) -> Array: +def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyArray: """Create an array of zeros like another array. Parameters diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 51e638edd8..6e0dc90100 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -138,6 +138,7 @@ from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar from zarr.core.group import AsyncGroup from zarr.storage import StoreLike + from zarr.types import AnyArray # Array and AsyncArray are defined in the base ``zarr`` namespace @@ -1844,12 +1845,12 @@ def _info( # TODO: Array can be a frozen data class again once property setters (e.g. shape) are removed @dataclass(frozen=False) -class Array: +class Array(Generic[T_ArrayMetadata]): """ A Zarr array. """ - _async_array: AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] + _async_array: AsyncArray[T_ArrayMetadata] @classmethod @deprecated("Use zarr.create_array instead.", category=ZarrDeprecationWarning) @@ -1882,7 +1883,7 @@ def create( # runtime overwrite: bool = False, config: ArrayConfigLike | None = None, - ) -> Array: + ) -> AnyArray: """Creates a new Array instance from an initialized store. .. deprecated:: 3.0.0 @@ -2011,7 +2012,7 @@ def _create( # runtime overwrite: bool = False, config: ArrayConfigLike | None = None, - ) -> Array: + ) -> AnyArray: """Creates a new Array instance from an initialized store. See :func:`Array.create` for more details. Deprecated in favor of :func:`zarr.create_array`. @@ -2037,14 +2038,14 @@ def _create( config=config, ), ) - return cls(async_array) + return Array(async_array) @classmethod def from_dict( cls, store_path: StorePath, data: dict[str, JSON], - ) -> Array: + ) -> AnyArray: """ Create a Zarr array from a dictionary. @@ -2068,13 +2069,13 @@ def from_dict( If the dictionary data is invalid or missing required fields for array creation. """ async_array = AsyncArray.from_dict(store_path=store_path, data=data) - return cls(async_array) + return Array(async_array) @classmethod def open( cls, store: StoreLike, - ) -> Array: + ) -> AnyArray: """Opens an existing Array from a store. Parameters @@ -2088,7 +2089,7 @@ def open( Array opened from the store. """ async_array = sync(AsyncArray.open(store)) - return cls(async_array) + return Array(async_array) @property def store(self) -> Store: @@ -3743,7 +3744,7 @@ def append(self, data: npt.ArrayLike, axis: int = 0) -> tuple[int, ...]: """ return sync(self._async_array.append(data, axis=axis)) - def update_attributes(self, new_attributes: dict[str, JSON]) -> Array: + def update_attributes(self, new_attributes: dict[str, JSON]) -> Self: """ Update the array's attributes. @@ -3768,11 +3769,8 @@ def update_attributes(self, new_attributes: dict[str, JSON]) -> Array: - The updated attributes will be merged with existing attributes, and any conflicts will be overwritten by the new values. """ - # TODO: remove this cast when type inference improves new_array = sync(self._async_array.update_attributes(new_attributes)) - # TODO: remove this cast when type inference improves - _new_array = cast("AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]", new_array) - return type(self)(_new_array) + return type(self)(new_array) def __repr__(self) -> str: return f"" @@ -3927,7 +3925,7 @@ class ShardsConfigParam(TypedDict): async def from_array( store: str | StoreLike, *, - data: Array | npt.ArrayLike, + data: AnyArray | npt.ArrayLike, write_data: bool = True, name: str | None = None, chunks: Literal["auto", "keep"] | tuple[int, ...] = "keep", @@ -4164,7 +4162,7 @@ async def from_array( if isinstance(data, Array): async def _copy_array_region( - chunk_coords: tuple[int, ...] | slice, _data: Array + chunk_coords: tuple[int, ...] | slice, _data: AnyArray ) -> None: arr = await _data._async_array.getitem(chunk_coords) await result.setitem(chunk_coords, arr) @@ -4592,7 +4590,7 @@ async def create_array( def _parse_keep_array_attr( - data: Array | npt.ArrayLike, + data: AnyArray | npt.ArrayLike, chunks: Literal["auto", "keep"] | tuple[int, ...], shards: ShardsLike | None | Literal["keep"], filters: FiltersLike | Literal["keep"], diff --git a/src/zarr/core/attributes.py b/src/zarr/core/attributes.py index e699c4f66d..a4fe80020f 100644 --- a/src/zarr/core/attributes.py +++ b/src/zarr/core/attributes.py @@ -8,12 +8,12 @@ if TYPE_CHECKING: from collections.abc import Iterator - from zarr.core.array import Array from zarr.core.group import Group + from zarr.types import AnyArray class Attributes(MutableMapping[str, JSON]): - def __init__(self, obj: Array | Group) -> None: + def __init__(self, obj: AnyArray | Group) -> None: # key=".zattrs", read_only=False, cache=True, synchronizer=None self._obj = obj diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 4c14fb357c..64ed169ff4 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -78,6 +78,7 @@ from zarr.core.chunk_key_encodings import ChunkKeyEncodingLike from zarr.core.common import MemoryOrder from zarr.core.dtype import ZDTypeLike + from zarr.types import AnyArray, ArrayV2, ArrayV3 logger = logging.getLogger("zarr.group") @@ -111,7 +112,11 @@ def parse_attributes(data: Any) -> dict[str, Any]: @overload -def _parse_async_node(node: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]) -> Array: ... +def _parse_async_node(node: AsyncArray[ArrayV3Metadata]) -> ArrayV3: ... + + +@overload +def _parse_async_node(node: AsyncArray[ArrayV2Metadata]) -> ArrayV2: ... @overload @@ -120,7 +125,7 @@ def _parse_async_node(node: AsyncGroup) -> Group: ... def _parse_async_node( node: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup, -) -> Array | Group: +) -> AnyArray | Group: """Wrap an AsyncArray in an Array, or an AsyncGroup in a Group.""" if isinstance(node, AsyncArray): return Array(node) @@ -1877,7 +1882,7 @@ def open( obj = sync(AsyncGroup.open(store, zarr_format=zarr_format)) return cls(obj) - def __getitem__(self, path: str) -> Array | Group: + def __getitem__(self, path: str) -> AnyArray | Group: """Obtain a group member. Parameters @@ -1910,7 +1915,7 @@ def __getitem__(self, path: str) -> Array | Group: else: return Group(obj) - def get(self, path: str, default: DefaultT | None = None) -> Array | Group | DefaultT | None: + def get(self, path: str, default: DefaultT | None = None) -> AnyArray | Group | DefaultT | None: """Obtain a group member, returning default if not found. Parameters @@ -2146,7 +2151,7 @@ def nmembers(self, max_depth: int | None = 0) -> int: def members( self, max_depth: int | None = 0, *, use_consolidated_for_children: bool = True - ) -> tuple[tuple[str, Array | Group], ...]: + ) -> tuple[tuple[str, AnyArray | Group], ...]: """ Returns an AsyncGenerator over the arrays and groups contained in this group. This method requires that `store_path.store` supports directory listing. @@ -2182,7 +2187,7 @@ def create_hierarchy( nodes: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata], *, overwrite: bool = False, - ) -> Iterator[tuple[str, Group | Array]]: + ) -> Iterator[tuple[str, Group | AnyArray]]: """ Create a hierarchy of arrays or groups rooted at this group. @@ -2321,7 +2326,7 @@ def group_values(self) -> Generator[Group, None]: for _, group in self.groups(): yield group - def arrays(self) -> Generator[tuple[str, Array], None]: + def arrays(self) -> Generator[tuple[str, AnyArray], None]: """Return the sub-arrays of this group as a generator of (name, array) pairs Examples @@ -2352,7 +2357,7 @@ def array_keys(self) -> Generator[str, None]: for name, _ in self.arrays(): yield name - def array_values(self) -> Generator[Array, None]: + def array_values(self) -> Generator[AnyArray, None]: """Return an iterator over group members. Examples @@ -2438,7 +2443,7 @@ def require_groups(self, *names: str) -> tuple[Group, ...]: """ return tuple(map(Group, self._sync(self._async_group.require_groups(*names)))) - def create(self, *args: Any, **kwargs: Any) -> Array: + def create(self, *args: Any, **kwargs: Any) -> AnyArray: # Backwards compatibility for 2.x return self.create_array(*args, **kwargs) @@ -2464,7 +2469,7 @@ def create_array( overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, - ) -> Array: + ) -> AnyArray: """Create an array within this group. This method lightly wraps :func:`zarr.core.array.create_array`. @@ -2592,7 +2597,7 @@ def create_array( ) @deprecated("Use Group.create_array instead.", category=ZarrDeprecationWarning) - def create_dataset(self, name: str, **kwargs: Any) -> Array: + def create_dataset(self, name: str, **kwargs: Any) -> AnyArray: """Create an array. .. deprecated:: 3.0.0 @@ -2616,7 +2621,7 @@ def create_dataset(self, name: str, **kwargs: Any) -> Array: return Array(self._sync(self._async_group.create_dataset(name, **kwargs))) @deprecated("Use Group.require_array instead.", category=ZarrDeprecationWarning) - def require_dataset(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> Array: + def require_dataset(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyArray: """Obtain an array, creating if it doesn't exist. .. deprecated:: 3.0.0 @@ -2640,7 +2645,7 @@ def require_dataset(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> Arra """ return Array(self._sync(self._async_group.require_array(name, shape=shape, **kwargs))) - def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> Array: + def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyArray: """Obtain an array, creating if it doesn't exist. Other `kwargs` are as per :func:`zarr.Group.create_array`. @@ -2658,7 +2663,7 @@ def require_array(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> Array: """ return Array(self._sync(self._async_group.require_array(name, shape=shape, **kwargs))) - def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: + def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an empty array with the specified shape in this Group. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -2679,7 +2684,7 @@ def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: """ return Array(self._sync(self._async_group.empty(name=name, shape=shape, **kwargs))) - def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: + def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. Parameters @@ -2698,7 +2703,7 @@ def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: """ return Array(self._sync(self._async_group.zeros(name=name, shape=shape, **kwargs))) - def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: + def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyArray: """Create an array, with one being used as the default value for uninitialized portions of the array. Parameters @@ -2719,7 +2724,7 @@ def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Array: def full( self, *, name: str, shape: tuple[int, ...], fill_value: Any | None, **kwargs: Any - ) -> Array: + ) -> AnyArray: """Create an array, with "fill_value" being used as the default value for uninitialized portions of the array. Parameters @@ -2744,7 +2749,7 @@ def full( ) ) - def empty_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> Array: + def empty_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create an empty sub-array like `data`. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -2770,7 +2775,7 @@ def empty_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> """ return Array(self._sync(self._async_group.empty_like(name=name, data=data, **kwargs))) - def zeros_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> Array: + def zeros_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array of zeros like `data`. Parameters @@ -2790,7 +2795,7 @@ def zeros_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> return Array(self._sync(self._async_group.zeros_like(name=name, data=data, **kwargs))) - def ones_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> Array: + def ones_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array of ones like `data`. Parameters @@ -2809,7 +2814,7 @@ def ones_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> A """ return Array(self._sync(self._async_group.ones_like(name=name, data=data, **kwargs))) - def full_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> Array: + def full_like(self, *, name: str, data: async_api.ArrayLike, **kwargs: Any) -> AnyArray: """Create a sub-array like `data` filled with the `fill_value` of `data` . Parameters @@ -2859,7 +2864,7 @@ def array( overwrite: bool = False, config: ArrayConfig | ArrayConfigLike | None = None, data: npt.ArrayLike | None = None, - ) -> Array: + ) -> AnyArray: """Create an array within this group. .. deprecated:: 3.0.0 diff --git a/src/zarr/core/indexing.py b/src/zarr/core/indexing.py index 16cb271601..12ab2e30e2 100644 --- a/src/zarr/core/indexing.py +++ b/src/zarr/core/indexing.py @@ -30,9 +30,10 @@ from zarr.core.metadata import T_ArrayMetadata if TYPE_CHECKING: - from zarr.core.array import Array, AsyncArray + from zarr.core.array import AsyncArray from zarr.core.buffer import NDArrayLikeOrScalar from zarr.core.chunk_grids import ChunkGrid + from zarr.types import AnyArray IntSequence = list[int] | npt.NDArray[np.intp] @@ -78,7 +79,7 @@ def err_too_many_indices(selection: Any, shape: tuple[int, ...]) -> None: raise IndexError(f"too many indices for array; expected {len(shape)}, got {len(selection)}") -def _zarr_array_to_int_or_bool_array(arr: Array) -> npt.NDArray[np.intp] | npt.NDArray[np.bool_]: +def _zarr_array_to_int_or_bool_array(arr: AnyArray) -> npt.NDArray[np.intp] | npt.NDArray[np.bool_]: if arr.dtype.kind in ("i", "b"): return np.asarray(arr) else: @@ -930,10 +931,10 @@ def __iter__(self) -> Iterator[ChunkProjection]: @dataclass(frozen=True) class OIndex: - array: Array + array: AnyArray # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool - def __getitem__(self, selection: OrthogonalSelection | Array) -> NDArrayLikeOrScalar: + def __getitem__(self, selection: OrthogonalSelection | AnyArray) -> NDArrayLikeOrScalar: from zarr.core.array import Array # if input is a Zarr array, we materialize it now. @@ -960,7 +961,7 @@ def __setitem__(self, selection: OrthogonalSelection, value: npt.ArrayLike) -> N class AsyncOIndex(Generic[T_ArrayMetadata]): array: AsyncArray[T_ArrayMetadata] - async def getitem(self, selection: OrthogonalSelection | Array) -> NDArrayLikeOrScalar: + async def getitem(self, selection: OrthogonalSelection | AnyArray) -> NDArrayLikeOrScalar: from zarr.core.array import Array # if input is a Zarr array, we materialize it now. @@ -1059,7 +1060,7 @@ def __iter__(self) -> Iterator[ChunkProjection]: @dataclass(frozen=True) class BlockIndex: - array: Array + array: AnyArray def __getitem__(self, selection: BasicSelection) -> NDArrayLikeOrScalar: fields, new_selection = pop_fields(selection) @@ -1250,11 +1251,11 @@ def __init__( @dataclass(frozen=True) class VIndex: - array: Array + array: AnyArray # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool def __getitem__( - self, selection: CoordinateSelection | MaskSelection | Array + self, selection: CoordinateSelection | MaskSelection | AnyArray ) -> NDArrayLikeOrScalar: from zarr.core.array import Array @@ -1291,7 +1292,7 @@ class AsyncVIndex(Generic[T_ArrayMetadata]): # TODO: develop Array generic and move zarr.Array[np.intp] | zarr.Array[np.bool_] to ArrayOfIntOrBool async def getitem( - self, selection: CoordinateSelection | MaskSelection | Array + self, selection: CoordinateSelection | MaskSelection | AnyArray ) -> NDArrayLikeOrScalar: # TODO deduplicate these internals with the sync version of getitem # TODO requires solving this circular sync issue: https://github.com/zarr-developers/zarr-python/pull/3083#discussion_r2230737448 diff --git a/src/zarr/core/metadata/__init__.py b/src/zarr/core/metadata/__init__.py index 43b5ec98fe..87a66d23f3 100644 --- a/src/zarr/core/metadata/__init__.py +++ b/src/zarr/core/metadata/__init__.py @@ -5,7 +5,7 @@ ArrayMetadata: TypeAlias = ArrayV2Metadata | ArrayV3Metadata ArrayMetadataDict: TypeAlias = ArrayV2MetadataDict | ArrayV3MetadataDict -T_ArrayMetadata = TypeVar("T_ArrayMetadata", ArrayV2Metadata, ArrayV3Metadata) +T_ArrayMetadata = TypeVar("T_ArrayMetadata", ArrayV2Metadata, ArrayV3Metadata, covariant=True) __all__ = [ "ArrayMetadata", diff --git a/src/zarr/core/sync_group.py b/src/zarr/core/sync_group.py index 39d8a17992..417362b122 100644 --- a/src/zarr/core/sync_group.py +++ b/src/zarr/core/sync_group.py @@ -13,14 +13,14 @@ from collections.abc import Iterator from zarr.abc.store import Store - from zarr.core.array import Array from zarr.core.common import ZarrFormat from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata + from zarr.types import AnyArray def create_nodes( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata] -) -> Iterator[tuple[str, Group | Array]]: +) -> Iterator[tuple[str, Group | AnyArray]]: """Create a collection of arrays and / or groups concurrently. Note: no attempt is made to validate that these arrays and / or groups collectively form a @@ -53,7 +53,7 @@ def create_hierarchy( store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, -) -> Iterator[tuple[str, Group | Array]]: +) -> Iterator[tuple[str, Group | AnyArray]]: """ Create a complete zarr hierarchy from a collection of metadata objects. @@ -115,7 +115,7 @@ def create_rooted_hierarchy( store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, -) -> Group | Array: +) -> Group | AnyArray: """ Create a Zarr hierarchy with a root, and return the root node, which could be a ``Group`` or ``Array`` instance. @@ -140,7 +140,7 @@ def create_rooted_hierarchy( return _parse_async_node(async_node) -def get_node(store: Store, path: str, zarr_format: ZarrFormat) -> Array | Group: +def get_node(store: Store, path: str, zarr_format: ZarrFormat) -> AnyArray | Group: """ Get an Array or Group from a path in a Store. diff --git a/src/zarr/testing/strategies.py b/src/zarr/testing/strategies.py index d0726c3dd9..862b38a562 100644 --- a/src/zarr/testing/strategies.py +++ b/src/zarr/testing/strategies.py @@ -23,6 +23,7 @@ from zarr.storage import MemoryStore, StoreLike from zarr.storage._common import _dereference_path from zarr.storage._utils import normalize_path +from zarr.types import AnyArray # Copied from Xarray _attr_keys = st.text(st.characters(), min_size=1) @@ -246,7 +247,7 @@ def arrays( arrays: st.SearchStrategy | None = None, attrs: st.SearchStrategy = attrs, zarr_formats: st.SearchStrategy = zarr_formats, -) -> Array: +) -> AnyArray: store = draw(stores, label="store") path = draw(paths, label="array parent") name = draw(array_names, label="array name") diff --git a/src/zarr/types.py b/src/zarr/types.py new file mode 100644 index 0000000000..faf44f4632 --- /dev/null +++ b/src/zarr/types.py @@ -0,0 +1,14 @@ +from typing import Any, TypeAlias + +from zarr.core.array import Array +from zarr.core.metadata.v2 import ArrayV2Metadata +from zarr.core.metadata.v3 import ArrayV3Metadata + +AnyArray: TypeAlias = Array[Any] +AnyArray.__doc__ = "A Zarr format 2 or 3 `Array`" + +ArrayV2: TypeAlias = Array[ArrayV2Metadata] +ArrayV2.__doc__ = "A Zarr format 2 `Array`" + +ArrayV3: TypeAlias = Array[ArrayV3Metadata] +ArrayV3.__doc__ = "A Zarr format 3 `Array`" diff --git a/tests/test_api.py b/tests/test_api.py index 5447a0aa39..d80bbc69ac 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -16,6 +16,7 @@ from zarr.abc.store import Store from zarr.core.common import JSON, MemoryOrder, ZarrFormat + from zarr.types import AnyArray import contextlib from typing import Literal @@ -152,7 +153,7 @@ def test_open_array_respects_write_empty_chunks_config(zarr_format: ZarrFormat) def test_open_normalized_path( memory_store: MemoryStore, path: str, node_type: Literal["array", "group"] ) -> None: - node: Group | Array + node: Group | AnyArray if node_type == "group": node = group(store=memory_store, path=path) elif node_type == "array": @@ -1409,7 +1410,7 @@ def test_no_overwrite_load(tmp_path: Path) -> None: zarr.zeros_like, ], ) -def test_auto_chunks(f: Callable[..., Array]) -> None: +def test_auto_chunks(f: Callable[..., AnyArray]) -> None: # Make sure chunks are set automatically across the public API # TODO: test shards with this test too shape = (1000, 1000) diff --git a/tests/test_api/test_asynchronous.py b/tests/test_api/test_asynchronous.py index 910fd2883c..451cb178f9 100644 --- a/tests/test_api/test_asynchronous.py +++ b/tests/test_api/test_asynchronous.py @@ -10,13 +10,14 @@ from zarr import create_array from zarr.api.asynchronous import _get_shape_chunks, _like_args, open from zarr.core.buffer.core import default_buffer_prototype +from zarr.types import AnyArray if TYPE_CHECKING: from typing import Any import numpy.typing as npt - from zarr.core.array import Array, AsyncArray + from zarr.core.array import AsyncArray from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata @@ -80,7 +81,10 @@ def test_get_shape_chunks( ], ) def test_like_args( - observed: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | Array | npt.NDArray[Any], + observed: AsyncArray[ArrayV2Metadata] + | AsyncArray[ArrayV3Metadata] + | AnyArray + | npt.NDArray[Any], expected: object, ) -> None: """ diff --git a/tests/test_array.py b/tests/test_array.py index a316ee127f..e78826dc20 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -69,6 +69,7 @@ ZarrUserWarning, ) from zarr.storage import LocalStore, MemoryStore, StorePath +from zarr.types import AnyArray from .test_dtype.conftest import zdtype_examples @@ -357,9 +358,9 @@ def test_storage_transformers(store: MemoryStore, zarr_format: ZarrFormat | str) Array.from_dict(StorePath(store), data=metadata_dict) -@pytest.mark.parametrize("test_cls", [Array, AsyncArray[Any]]) +@pytest.mark.parametrize("test_cls", [AnyArray, AsyncArray[Any]]) @pytest.mark.parametrize("nchunks", [2, 5, 10]) -def test_nchunks(test_cls: type[Array] | type[AsyncArray[Any]], nchunks: int) -> None: +def test_nchunks(test_cls: type[AnyArray] | type[AsyncArray[Any]], nchunks: int) -> None: """ Test that nchunks returns the number of chunks defined for the array. """ @@ -374,8 +375,8 @@ def test_nchunks(test_cls: type[Array] | type[AsyncArray[Any]], nchunks: int) -> assert observed == expected -@pytest.mark.parametrize("test_cls", [Array, AsyncArray[Any]]) -async def test_nchunks_initialized(test_cls: type[Array] | type[AsyncArray[Any]]) -> None: +@pytest.mark.parametrize("test_cls", [AnyArray, AsyncArray[Any]]) +async def test_nchunks_initialized(test_cls: type[AnyArray] | type[AsyncArray[Any]]) -> None: """ Test that nchunks_initialized accurately returns the number of stored chunks. """ @@ -1428,7 +1429,7 @@ async def test_with_data(impl: Literal["sync", "async"], store: Store) -> None: """ data = np.arange(10) name = "foo" - arr: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | Array + arr: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AnyArray if impl == "sync": arr = sync_api.create_array(store, name=name, data=data) stored = arr[:] @@ -1649,7 +1650,7 @@ async def test_from_array_arraylike( store: Store, chunks: Literal["auto", "keep"] | tuple[int, int], write_data: bool, - src: Array | npt.ArrayLike, + src: AnyArray | npt.ArrayLike, ) -> None: fill_value = 42 result = zarr.from_array( @@ -1734,7 +1735,7 @@ def test_roundtrip_numcodecs() -> None: assert metadata["codecs"] == expected -def _index_array(arr: Array, index: Any) -> Any: +def _index_array(arr: AnyArray, index: Any) -> Any: return arr[index] diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 4ce40e2cb0..269704d2a0 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -1,5 +1,5 @@ import json -from typing import Any +from typing import TYPE_CHECKING, Any import numpy as np import pytest @@ -10,6 +10,9 @@ from tests.conftest import deep_nan_equal from zarr.core.common import ZarrFormat +if TYPE_CHECKING: + from zarr.types import AnyArray + @pytest.mark.parametrize("zarr_format", [2, 3]) @pytest.mark.parametrize( @@ -74,7 +77,7 @@ def test_update_no_changes() -> None: @pytest.mark.parametrize("group", [True, False]) def test_del_works(group: bool) -> None: store = zarr.storage.MemoryStore() - z: zarr.Group | zarr.Array + z: zarr.Group | AnyArray if group: z = zarr.create_group(store) else: @@ -84,7 +87,7 @@ def test_del_works(group: bool) -> None: del z.attrs["a"] assert dict(z.attrs) == {"c": 4} - z2: zarr.Group | zarr.Array + z2: zarr.Group | AnyArray if group: z2 = zarr.open_group(store) else: diff --git a/tests/test_regression/test_v2_dtype_regression.py b/tests/test_regression/test_v2_dtype_regression.py index ffe273490d..4f3329e88c 100644 --- a/tests/test_regression/test_v2_dtype_regression.py +++ b/tests/test_regression/test_v2_dtype_regression.py @@ -13,11 +13,11 @@ import zarr.abc.codec import zarr.codecs as zarrcodecs from zarr.abc.numcodec import Numcodec -from zarr.core.array import Array from zarr.core.chunk_key_encodings import V2ChunkKeyEncoding from zarr.core.dtype.npy.bytes import VariableLengthBytes from zarr.core.dtype.npy.string import VariableLengthUTF8 from zarr.storage import LocalStore +from zarr.types import ArrayV2, ArrayV3 if TYPE_CHECKING: from zarr.core.dtype import ZDTypeLike @@ -106,7 +106,7 @@ class ArrayParams: @pytest.fixture -def source_array_v2(tmp_path: Path, request: pytest.FixtureRequest) -> Array: +def source_array_v2(tmp_path: Path, request: pytest.FixtureRequest) -> ArrayV2: """ Writes a zarr array to a temporary directory based on the provided ArrayParams. The array is returned. @@ -144,7 +144,7 @@ def source_array_v2(tmp_path: Path, request: pytest.FixtureRequest) -> Array: @pytest.fixture -def source_array_v3(tmp_path: Path, request: pytest.FixtureRequest) -> Array: +def source_array_v3(tmp_path: Path, request: pytest.FixtureRequest) -> ArrayV3: """ Writes a zarr array to a temporary directory based on the provided ArrayParams. The array is returned. @@ -198,7 +198,7 @@ def source_array_v3(tmp_path: Path, request: pytest.FixtureRequest) -> Array: "source_array_v2", array_cases_v2_18, indirect=True, ids=tuple(map(str, array_cases_v2_18)) ) @pytest.mark.parametrize("script_path", script_paths) -def test_roundtrip_v2(source_array_v2: Array, tmp_path: Path, script_path: Path) -> None: +def test_roundtrip_v2(source_array_v2: ArrayV2, tmp_path: Path, script_path: Path) -> None: out_path = tmp_path / "out" copy_op = subprocess.run( [ @@ -222,7 +222,7 @@ def test_roundtrip_v2(source_array_v2: Array, tmp_path: Path, script_path: Path) @pytest.mark.parametrize( "source_array_v3", array_cases_v3_08, indirect=True, ids=tuple(map(str, array_cases_v3_08)) ) -def test_roundtrip_v3(source_array_v3: Array, tmp_path: Path) -> None: +def test_roundtrip_v3(source_array_v3: ArrayV3, tmp_path: Path) -> None: script_path = Path(__file__).resolve().parent / "scripts" / "v3.0.8.py" out_path = tmp_path / "out" copy_op = subprocess.run( From 88e113def7afa05334e776ec1e91bd7f38ec219e Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 28 Jul 2025 15:57:53 +0100 Subject: [PATCH 2/5] Try fixing docstrings --- src/zarr/types.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zarr/types.py b/src/zarr/types.py index faf44f4632..f685505291 100644 --- a/src/zarr/types.py +++ b/src/zarr/types.py @@ -5,10 +5,10 @@ from zarr.core.metadata.v3 import ArrayV3Metadata AnyArray: TypeAlias = Array[Any] -AnyArray.__doc__ = "A Zarr format 2 or 3 `Array`" +"""A Zarr format 2 or 3 `Array`""" ArrayV2: TypeAlias = Array[ArrayV2Metadata] -ArrayV2.__doc__ = "A Zarr format 2 `Array`" +"""A Zarr format 2 `Array`""" ArrayV3: TypeAlias = Array[ArrayV3Metadata] -ArrayV3.__doc__ = "A Zarr format 3 `Array`" +"""A Zarr format 3 `Array`""" From 6ff833ddb5cd539dd4d81b2c96cf18d486dee991 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 28 Jul 2025 16:12:26 +0100 Subject: [PATCH 3/5] Add an AnyAsyncArray type --- src/zarr/api/asynchronous.py | 54 +++++----------- src/zarr/core/array.py | 24 ++++---- src/zarr/core/group.py | 96 +++++++++++------------------ src/zarr/types.py | 6 +- tests/test_api/test_asynchronous.py | 2 +- tests/test_array.py | 19 +++--- tests/test_codecs/test_codecs.py | 5 +- 7 files changed, 81 insertions(+), 125 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 2f6777a74a..ada922cdd0 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -37,7 +37,7 @@ GroupMetadata, create_hierarchy, ) -from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata, ArrayV3Metadata +from zarr.core.metadata import ArrayMetadataDict, ArrayV2Metadata from zarr.errors import ( GroupNotFoundError, NodeTypeValidationError, @@ -56,12 +56,10 @@ from zarr.core.buffer import NDArrayLikeOrScalar from zarr.core.chunk_key_encodings import ChunkKeyEncoding from zarr.storage import StoreLike - from zarr.types import AnyArray + from zarr.types import AnyArray, AnyAsyncArray # TODO: this type could use some more thought - ArrayLike: TypeAlias = ( - AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AnyArray | npt.NDArray[Any] - ) + ArrayLike: TypeAlias = AnyAsyncArray | AnyArray | npt.NDArray[Any] PathLike = str __all__ = [ @@ -313,7 +311,7 @@ async def open( path: str | None = None, storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to open_array -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup: +) -> AnyAsyncArray | AsyncGroup: """Convenience function to open a group or array using file-mode-like semantics. Parameters @@ -570,9 +568,7 @@ async def tree(grp: AsyncGroup, expand: bool | None = None, level: int | None = return await grp.tree(expand=expand, level=level) -async def array( - data: npt.ArrayLike | AnyArray, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyAsyncArray: """Create an array filled with `data`. Parameters @@ -904,7 +900,7 @@ async def create( storage_options: dict[str, Any] | None = None, config: ArrayConfigLike | None = None, **kwargs: Any, -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +) -> AnyAsyncArray: """Create an array. Parameters @@ -1076,9 +1072,7 @@ async def create( ) -async def empty( - shape: tuple[int, ...], **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an empty array with the specified shape. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1099,9 +1093,7 @@ async def empty( return await create(shape=shape, fill_value=None, **kwargs) -async def empty_like( - a: ArrayLike, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an empty array like `a`. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1128,9 +1120,7 @@ async def empty_like( # TODO: add type annotations for fill_value and kwargs -async def full( - shape: tuple[int, ...], fill_value: Any, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyAsyncArray: """Create an array, with `fill_value` being used as the default value for uninitialized portions of the array. @@ -1152,9 +1142,7 @@ async def full( # TODO: add type annotations for kwargs -async def full_like( - a: ArrayLike, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create a filled array like `a`. Parameters @@ -1175,9 +1163,7 @@ async def full_like( return await full(**like_kwargs) -async def ones( - shape: tuple[int, ...], **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with one being used as the default value for uninitialized portions of the array. @@ -1196,9 +1182,7 @@ async def ones( return await create(shape=shape, fill_value=1, **kwargs) -async def ones_like( - a: ArrayLike, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an array of ones like `a`. Parameters @@ -1225,7 +1209,7 @@ async def open_array( path: PathLike = "", storage_options: dict[str, Any] | None = None, **kwargs: Any, # TODO: type kwargs as valid args to save -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +) -> AnyAsyncArray: """Open an array using file-mode-like semantics. Parameters @@ -1273,9 +1257,7 @@ async def open_array( raise -async def open_like( - a: ArrayLike, path: str, **kwargs: Any -) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: +async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: """Open a persistent array like `a`. Parameters @@ -1298,9 +1280,7 @@ async def open_like( return await open_array(path=path, **like_kwargs) -async def zeros( - shape: tuple[int, ...], **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. @@ -1319,9 +1299,7 @@ async def zeros( return await create(shape=shape, fill_value=0, **kwargs) -async def zeros_like( - a: ArrayLike, **kwargs: Any -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: """Create an array of zeros like `a`. Parameters diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 6e0dc90100..b389e34e49 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -138,7 +138,7 @@ from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar from zarr.core.group import AsyncGroup from zarr.storage import StoreLike - from zarr.types import AnyArray + from zarr.types import AnyArray, AnyAsyncArray # Array and AsyncArray are defined in the base ``zarr`` namespace @@ -437,7 +437,7 @@ async def create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: ... + ) -> AnyAsyncArray: ... @classmethod @deprecated("Use zarr.api.asynchronous.create_array instead.", category=ZarrDeprecationWarning) @@ -471,7 +471,7 @@ async def create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Method to create a new asynchronous array instance. .. deprecated:: 3.0.0 @@ -612,7 +612,7 @@ async def _create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Method to create a new asynchronous array instance. See :func:`AsyncArray.create` for more details. Deprecated in favor of :func:`zarr.api.asynchronous.create_array`. @@ -634,7 +634,7 @@ async def _create( _chunks = normalize_chunks(chunk_shape, shape, item_size) config_parsed = parse_array_config(config) - result: AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] + result: AnyAsyncArray if zarr_format == 3: if dimension_separator is not None: raise ValueError( @@ -903,7 +903,7 @@ def from_dict( cls, store_path: StorePath, data: dict[str, JSON], - ) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: + ) -> AnyAsyncArray: """ Create a Zarr array from a dictionary, with support for both Zarr format 2 and 3 metadata. @@ -935,7 +935,7 @@ async def open( cls, store: StoreLike, zarr_format: ZarrFormat | None = 3, - ) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: + ) -> AnyAsyncArray: """ Async method to open an existing Zarr array from a given store. @@ -3830,7 +3830,7 @@ def info_complete(self) -> Any: async def chunks_initialized( - array: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], + array: AnyAsyncArray, ) -> tuple[str, ...]: """ Return the keys of the chunks that have been persisted to the storage backend. @@ -3862,7 +3862,7 @@ async def chunks_initialized( def _build_parents( - node: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup, + node: AnyAsyncArray | AsyncGroup, ) -> list[AsyncGroup]: from zarr.core.group import AsyncGroup, GroupMetadata @@ -3942,7 +3942,7 @@ async def from_array( storage_options: dict[str, Any] | None = None, overwrite: bool = False, config: ArrayConfig | ArrayConfigLike | None = None, -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +) -> AnyAsyncArray: """Create an array from an existing array or array-like. Parameters @@ -4205,7 +4205,7 @@ async def init_array( dimension_names: DimensionNames = None, overwrite: bool = False, config: ArrayConfigLike | None, -) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata]: +) -> AnyAsyncArray: """Create and persist an array metadata document. Parameters @@ -4426,7 +4426,7 @@ async def create_array( overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +) -> AnyAsyncArray: """Create an array. Parameters diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 64ed169ff4..f74836c9e8 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -78,7 +78,7 @@ from zarr.core.chunk_key_encodings import ChunkKeyEncodingLike from zarr.core.common import MemoryOrder from zarr.core.dtype import ZDTypeLike - from zarr.types import AnyArray, ArrayV2, ArrayV3 + from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3 logger = logging.getLogger("zarr.group") @@ -124,7 +124,7 @@ def _parse_async_node(node: AsyncGroup) -> Group: ... def _parse_async_node( - node: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup, + node: AnyAsyncArray | AsyncGroup, ) -> AnyArray | Group: """Wrap an AsyncArray in an Array, or an AsyncGroup in a Group.""" if isinstance(node, AsyncArray): @@ -701,7 +701,7 @@ async def setitem(self, key: str, value: Any) -> None: async def getitem( self, key: str, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup: + ) -> AnyAsyncArray | AsyncGroup: """ Get a subarray or subgroup from the group. @@ -729,7 +729,7 @@ async def getitem( def _getitem_consolidated( self, store_path: StorePath, key: str, prefix: str - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup: + ) -> AnyAsyncArray | AsyncGroup: # getitem, in the special case where we have consolidated metadata. # Note that this is a regular def (non async) function. # This shouldn't do any additional I/O. @@ -792,7 +792,7 @@ async def delitem(self, key: str) -> None: async def get( self, key: str, default: DefaultT | None = None - ) -> AsyncArray[Any] | AsyncGroup | DefaultT | None: + ) -> AnyAsyncArray | AsyncGroup | DefaultT | None: """Obtain a group member, returning default if not found. Parameters @@ -987,9 +987,7 @@ async def require_group(self, name: str, overwrite: bool = False) -> AsyncGroup: grp = await self.create_group(name, overwrite=True) else: try: - item: ( - AsyncGroup | AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] - ) = await self.getitem(name) + item: AsyncGroup | AnyAsyncArray = await self.getitem(name) if not isinstance(item, AsyncGroup): raise TypeError( f"Incompatible object ({item.__class__.__name__}) already exists" @@ -1038,7 +1036,7 @@ async def create_array( overwrite: bool = False, config: ArrayConfigLike | None = None, write_data: bool = True, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create an array within this group. This method lightly wraps :func:`zarr.core.array.create_array`. @@ -1163,9 +1161,7 @@ async def create_array( ) @deprecated("Use AsyncGroup.create_array instead.", category=ZarrDeprecationWarning) - async def create_dataset( - self, name: str, *, shape: ShapeLike, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + async def create_dataset(self, name: str, *, shape: ShapeLike, **kwargs: Any) -> AnyAsyncArray: """Create an array. .. deprecated:: 3.0.0 @@ -1205,7 +1201,7 @@ async def require_dataset( dtype: npt.DTypeLike = None, exact: bool = False, **kwargs: Any, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Obtain an array, creating if it doesn't exist. .. deprecated:: 3.0.0 @@ -1242,7 +1238,7 @@ async def require_array( dtype: npt.DTypeLike = None, exact: bool = False, **kwargs: Any, - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Obtain an array, creating if it doesn't exist. Other `kwargs` are as per :func:`zarr.AsyncGroup.create_dataset`. @@ -1352,7 +1348,7 @@ async def members( *, use_consolidated_for_children: bool = True, ) -> AsyncGenerator[ - tuple[str, AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup], + tuple[str, AnyAsyncArray | AsyncGroup], None, ]: """ @@ -1391,7 +1387,7 @@ async def members( def _members_consolidated( self, max_depth: int | None, prefix: str = "" ) -> Generator[ - tuple[str, AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup], + tuple[str, AnyAsyncArray | AsyncGroup], None, ]: consolidated_metadata = self.metadata.consolidated_metadata @@ -1416,9 +1412,7 @@ def _members_consolidated( async def _members( self, max_depth: int | None, *, use_consolidated_for_children: bool = True - ) -> AsyncGenerator[ - tuple[str, AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup], None - ]: + ) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: skip_keys: tuple[str, ...] if self.metadata.zarr_format == 2: skip_keys = (".zattrs", ".zgroup", ".zarray", ".zmetadata") @@ -1458,9 +1452,7 @@ async def create_hierarchy( nodes: dict[str, ArrayV2Metadata | ArrayV3Metadata | GroupMetadata], *, overwrite: bool = False, - ) -> AsyncIterator[ - tuple[str, AsyncGroup | AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]] - ]: + ) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """ Create a hierarchy of arrays or groups rooted at this group. @@ -1574,9 +1566,7 @@ async def group_values(self) -> AsyncGenerator[AsyncGroup, None]: async def arrays( self, - ) -> AsyncGenerator[ - tuple[str, AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]], None - ]: + ) -> AsyncGenerator[tuple[str, AnyAsyncArray], None]: """Iterate over arrays.""" async for key, value in self.members(): if isinstance(value, AsyncArray): @@ -1589,7 +1579,7 @@ async def array_keys(self) -> AsyncGenerator[str, None]: async def array_values( self, - ) -> AsyncGenerator[AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata], None]: + ) -> AsyncGenerator[AnyAsyncArray, None]: """Iterate over array values.""" async for _, array in self.arrays(): yield array @@ -1619,9 +1609,7 @@ async def tree(self, expand: bool | None = None, level: int | None = None) -> An raise NotImplementedError("'expand' is not yet implemented.") return await group_tree_async(self, max_depth=level) - async def empty( - self, *, name: str, shape: tuple[int, ...], **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + async def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an empty array with the specified shape in this Group. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1642,9 +1630,7 @@ async def empty( """ return await async_api.empty(shape=shape, store=self.store_path, path=name, **kwargs) - async def zeros( - self, *, name: str, shape: tuple[int, ...], **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + async def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with zero being used as the default value for uninitialized portions of the array. Parameters @@ -1663,9 +1649,7 @@ async def zeros( """ return await async_api.zeros(shape=shape, store=self.store_path, path=name, **kwargs) - async def ones( - self, *, name: str, shape: tuple[int, ...], **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + async def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: """Create an array, with one being used as the default value for uninitialized portions of the array. Parameters @@ -1686,7 +1670,7 @@ async def ones( async def full( self, *, name: str, shape: tuple[int, ...], fill_value: Any | None, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create an array, with "fill_value" being used as the default value for uninitialized portions of the array. Parameters @@ -1715,7 +1699,7 @@ async def full( async def empty_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create an empty sub-array like `data`. The contents will be filled with the array's fill value or zeros if no fill value is provided. @@ -1737,7 +1721,7 @@ async def empty_like( async def zeros_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create a sub-array of zeros like `data`. Parameters @@ -1758,7 +1742,7 @@ async def zeros_like( async def ones_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create a sub-array of ones like `data`. Parameters @@ -1779,7 +1763,7 @@ async def ones_like( async def full_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any - ) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: + ) -> AnyAsyncArray: """Create a sub-array like `data` filled with the `fill_value` of `data` . Parameters @@ -2992,9 +2976,7 @@ async def create_hierarchy( store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, -) -> AsyncIterator[ - tuple[str, AsyncGroup | AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]] -]: +) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """ Create a complete zarr hierarchy from a collection of metadata objects. @@ -3156,9 +3138,7 @@ async def create_nodes( *, store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], -) -> AsyncIterator[ - tuple[str, AsyncGroup | AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]] -]: +) -> AsyncIterator[tuple[str, AsyncGroup | AnyAsyncArray]]: """Create a collection of arrays and / or groups concurrently. Note: no attempt is made to validate that these arrays and / or groups collectively form a @@ -3336,7 +3316,7 @@ def _ensure_consistent_zarr_format( async def _getitem_semaphore( node: AsyncGroup, key: str, semaphore: asyncio.Semaphore | None -) -> AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup: +) -> AnyAsyncArray | AsyncGroup: """ Wrap Group.getitem with an optional semaphore. @@ -3356,9 +3336,7 @@ async def _iter_members( node: AsyncGroup, skip_keys: tuple[str, ...], semaphore: asyncio.Semaphore | None, -) -> AsyncGenerator[ - tuple[str, AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup], None -]: +) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: """ Iterate over the arrays and groups contained in a group. @@ -3373,7 +3351,7 @@ async def _iter_members( Yields ------ - tuple[str, AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup] + tuple[str, AnyAsyncArray | AsyncGroup] """ # retrieve keys from storage @@ -3412,9 +3390,7 @@ async def _iter_members_deep( skip_keys: tuple[str, ...], semaphore: asyncio.Semaphore | None = None, use_consolidated_for_children: bool = True, -) -> AsyncGenerator[ - tuple[str, AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup], None -]: +) -> AsyncGenerator[tuple[str, AnyAsyncArray | AsyncGroup], None]: """ Iterate over the arrays and groups contained in a group, and optionally the arrays and groups contained in those groups. @@ -3437,7 +3413,7 @@ async def _iter_members_deep( Yields ------ - tuple[str, AsyncArray[ArrayV3Metadata] | AsyncArray[ArrayV2Metadata] | AsyncGroup] + tuple[str, AnyAsyncArray | AsyncGroup] """ to_recurse = {} @@ -3452,7 +3428,7 @@ async def _iter_members_deep( if ( is_group and not use_consolidated_for_children - and node.metadata.consolidated_metadata is not None # type: ignore [union-attr] + and node.metadata.consolidated_metadata is not None ): node = cast("AsyncGroup", node) # We've decided not to trust consolidated metadata at this point, because we're @@ -3597,7 +3573,7 @@ def _build_node(*, store: Store, path: str, metadata: GroupMetadata) -> AsyncGro def _build_node( *, store: Store, path: str, metadata: ArrayV3Metadata | ArrayV2Metadata | GroupMetadata -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup: +) -> AnyAsyncArray | AsyncGroup: """ Take a metadata object and return a node (AsyncArray or AsyncGroup). """ @@ -3649,9 +3625,7 @@ async def _get_node_v3(store: Store, path: str) -> AsyncArray[ArrayV3Metadata] | return _build_node(store=store, path=path, metadata=metadata) -async def get_node( - store: Store, path: str, zarr_format: ZarrFormat -) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup: +async def get_node(store: Store, path: str, zarr_format: ZarrFormat) -> AnyAsyncArray | AsyncGroup: """ Get an AsyncArray or AsyncGroup from a path in a Store. @@ -3729,7 +3703,7 @@ async def create_rooted_hierarchy( store: Store, nodes: dict[str, GroupMetadata | ArrayV2Metadata | ArrayV3Metadata], overwrite: bool = False, -) -> AsyncGroup | AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]: +) -> AsyncGroup | AnyAsyncArray: """ Create an ``AsyncGroup`` or ``AsyncArray`` from a store and a dict of metadata documents. This function ensures that its input contains a specification of a root node, diff --git a/src/zarr/types.py b/src/zarr/types.py index f685505291..2164710f1c 100644 --- a/src/zarr/types.py +++ b/src/zarr/types.py @@ -1,9 +1,13 @@ from typing import Any, TypeAlias -from zarr.core.array import Array +from zarr.core.array import Array, AsyncArray from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata +AnyAsyncArray: TypeAlias = AsyncArray[Any] +"""A Zarr format 2 or 3 `AsyncArray`""" + + AnyArray: TypeAlias = Array[Any] """A Zarr format 2 or 3 `Array`""" diff --git a/tests/test_api/test_asynchronous.py b/tests/test_api/test_asynchronous.py index 451cb178f9..025c130a87 100644 --- a/tests/test_api/test_asynchronous.py +++ b/tests/test_api/test_asynchronous.py @@ -10,7 +10,6 @@ from zarr import create_array from zarr.api.asynchronous import _get_shape_chunks, _like_args, open from zarr.core.buffer.core import default_buffer_prototype -from zarr.types import AnyArray if TYPE_CHECKING: from typing import Any @@ -19,6 +18,7 @@ from zarr.core.array import AsyncArray from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata + from zarr.types import AnyArray @dataclass diff --git a/tests/test_array.py b/tests/test_array.py index e78826dc20..0d70bfc39d 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -19,7 +19,7 @@ import zarr.api.asynchronous import zarr.api.synchronous as sync_api from tests.conftest import skip_object_dtype -from zarr import Array, AsyncArray, Group +from zarr import Array, Group from zarr.abc.store import Store from zarr.codecs import ( BytesCodec, @@ -29,6 +29,7 @@ ) from zarr.core._info import ArrayInfo from zarr.core.array import ( + AsyncArray, CompressorsLike, FiltersLike, _parse_chunk_encoding_v2, @@ -61,7 +62,6 @@ from zarr.core.group import AsyncGroup from zarr.core.indexing import BasicIndexer from zarr.core.metadata.v2 import ArrayV2Metadata -from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import sync from zarr.errors import ( ContainsArrayError, @@ -69,13 +69,12 @@ ZarrUserWarning, ) from zarr.storage import LocalStore, MemoryStore, StorePath -from zarr.types import AnyArray +from zarr.types import AnyArray, AnyAsyncArray from .test_dtype.conftest import zdtype_examples if TYPE_CHECKING: from zarr.abc.codec import CodecJSON_V3 - from zarr.core.metadata.v3 import ArrayV3Metadata @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) @@ -358,9 +357,9 @@ def test_storage_transformers(store: MemoryStore, zarr_format: ZarrFormat | str) Array.from_dict(StorePath(store), data=metadata_dict) -@pytest.mark.parametrize("test_cls", [AnyArray, AsyncArray[Any]]) +@pytest.mark.parametrize("test_cls", [AnyArray, AnyAsyncArray]) @pytest.mark.parametrize("nchunks", [2, 5, 10]) -def test_nchunks(test_cls: type[AnyArray] | type[AsyncArray[Any]], nchunks: int) -> None: +def test_nchunks(test_cls: type[AnyArray] | type[AnyAsyncArray], nchunks: int) -> None: """ Test that nchunks returns the number of chunks defined for the array. """ @@ -375,8 +374,8 @@ def test_nchunks(test_cls: type[AnyArray] | type[AsyncArray[Any]], nchunks: int) assert observed == expected -@pytest.mark.parametrize("test_cls", [AnyArray, AsyncArray[Any]]) -async def test_nchunks_initialized(test_cls: type[AnyArray] | type[AsyncArray[Any]]) -> None: +@pytest.mark.parametrize("test_cls", [AnyArray, AnyAsyncArray]) +async def test_nchunks_initialized(test_cls: type[AnyArray] | type[AnyAsyncArray]) -> None: """ Test that nchunks_initialized accurately returns the number of stored chunks. """ @@ -1332,7 +1331,7 @@ async def test_v2_chunk_encoding( ) -> None: if dtype == "str" and filters != "auto": pytest.skip("Only the auto filters are compatible with str dtype in this test.") - arr = await create_array( + arr: AsyncArray[ArrayV2Metadata] = await create_array( store=store, dtype=dtype, shape=(10,), @@ -1429,7 +1428,7 @@ async def test_with_data(impl: Literal["sync", "async"], store: Store) -> None: """ data = np.arange(10) name = "foo" - arr: AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AnyArray + arr: AnyAsyncArray | AnyArray if impl == "sync": arr = sync_api.create_array(store, name=name, data=data) stored = arr[:] diff --git a/tests/test_codecs/test_codecs.py b/tests/test_codecs/test_codecs.py index dfedbb83de..59455dbe33 100644 --- a/tests/test_codecs/test_codecs.py +++ b/tests/test_codecs/test_codecs.py @@ -29,11 +29,12 @@ from zarr.abc.store import Store from zarr.core.buffer.core import NDArrayLikeOrScalar from zarr.core.common import MemoryOrder + from zarr.types import AnyAsyncArray @dataclass(frozen=True) class _AsyncArrayProxy: - array: AsyncArray[Any] + array: AnyAsyncArray def __getitem__(self, selection: BasicSelection) -> _AsyncArraySelectionProxy: return _AsyncArraySelectionProxy(self.array, selection) @@ -41,7 +42,7 @@ def __getitem__(self, selection: BasicSelection) -> _AsyncArraySelectionProxy: @dataclass(frozen=True) class _AsyncArraySelectionProxy: - array: AsyncArray[Any] + array: AnyAsyncArray selection: BasicSelection async def get(self) -> NDArrayLikeOrScalar: From d5c77f37134ad4d64fbce0846d2aa76a3eba464f Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 28 Jul 2025 16:17:15 +0100 Subject: [PATCH 4/5] Add async array v2 and v3 types --- src/zarr/core/array.py | 18 +++++++++--------- src/zarr/core/group.py | 18 +++++++----------- src/zarr/types.py | 5 +++++ 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index b389e34e49..c746f12d82 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -138,7 +138,7 @@ from zarr.core.dtype.wrapper import TBaseDType, TBaseScalar from zarr.core.group import AsyncGroup from zarr.storage import StoreLike - from zarr.types import AnyArray, AnyAsyncArray + from zarr.types import AnyArray, AnyAsyncArray, AsyncArrayV2, AsyncArrayV3 # Array and AsyncArray are defined in the base ``zarr`` namespace @@ -292,7 +292,7 @@ class AsyncArray(Generic[T_ArrayMetadata]): @overload def __init__( - self: AsyncArray[ArrayV2Metadata], + self: AsyncArrayV2, metadata: ArrayV2Metadata | ArrayV2MetadataDict, store_path: StorePath, config: ArrayConfigLike | None = None, @@ -300,7 +300,7 @@ def __init__( @overload def __init__( - self: AsyncArray[ArrayV3Metadata], + self: AsyncArrayV3, metadata: ArrayV3Metadata | ArrayV3MetadataDict, store_path: StorePath, config: ArrayConfigLike | None = None, @@ -346,7 +346,7 @@ async def create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV2Metadata]: ... + ) -> AsyncArrayV2: ... # this overload defines the function signature when zarr_format is 3 @overload @@ -375,7 +375,7 @@ async def create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV3Metadata]: ... + ) -> AsyncArrayV3: ... @overload @classmethod @@ -403,7 +403,7 @@ async def create( overwrite: bool = False, data: npt.ArrayLike | None = None, config: ArrayConfigLike | None = None, - ) -> AsyncArray[ArrayV3Metadata]: ... + ) -> AsyncArrayV3: ... @overload @classmethod @@ -778,7 +778,7 @@ async def _create_v3( dimension_names: DimensionNames = None, attributes: dict[str, JSON] | None = None, overwrite: bool = False, - ) -> AsyncArray[ArrayV3Metadata]: + ) -> AsyncArrayV3: if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() @@ -859,7 +859,7 @@ async def _create_v2( compressor: CompressorLike = "auto", attributes: dict[str, JSON] | None = None, overwrite: bool = False, - ) -> AsyncArray[ArrayV2Metadata]: + ) -> AsyncArrayV2: if overwrite: if store_path.store.supports_deletes: await store_path.delete_dir() @@ -919,7 +919,7 @@ def from_dict( Returns ------- - AsyncArray[ArrayV3Metadata] or AsyncArray[ArrayV2Metadata] + AsyncArrayV3 or AsyncArrayV2 The created Zarr array, either using Zarr format 2 or 3 metadata based on the provided data. Raises diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index f74836c9e8..b434d8c56e 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -78,7 +78,7 @@ from zarr.core.chunk_key_encodings import ChunkKeyEncodingLike from zarr.core.common import MemoryOrder from zarr.core.dtype import ZDTypeLike - from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3 + from zarr.types import AnyArray, AnyAsyncArray, ArrayV2, ArrayV3, AsyncArrayV2, AsyncArrayV3 logger = logging.getLogger("zarr.group") @@ -112,11 +112,11 @@ def parse_attributes(data: Any) -> dict[str, Any]: @overload -def _parse_async_node(node: AsyncArray[ArrayV3Metadata]) -> ArrayV3: ... +def _parse_async_node(node: AsyncArrayV3) -> ArrayV3: ... @overload -def _parse_async_node(node: AsyncArray[ArrayV2Metadata]) -> ArrayV2: ... +def _parse_async_node(node: AsyncArrayV2) -> ArrayV2: ... @overload @@ -3556,15 +3556,11 @@ def _build_metadata_v2( @overload -def _build_node( - *, store: Store, path: str, metadata: ArrayV2Metadata -) -> AsyncArray[ArrayV2Metadata]: ... +def _build_node(*, store: Store, path: str, metadata: ArrayV2Metadata) -> AsyncArrayV2: ... @overload -def _build_node( - *, store: Store, path: str, metadata: ArrayV3Metadata -) -> AsyncArray[ArrayV3Metadata]: ... +def _build_node(*, store: Store, path: str, metadata: ArrayV3Metadata) -> AsyncArrayV3: ... @overload @@ -3587,7 +3583,7 @@ def _build_node( raise ValueError(f"Unexpected metadata type: {type(metadata)}") # pragma: no cover -async def _get_node_v2(store: Store, path: str) -> AsyncArray[ArrayV2Metadata] | AsyncGroup: +async def _get_node_v2(store: Store, path: str) -> AsyncArrayV2 | AsyncGroup: """ Read a Zarr v2 AsyncArray or AsyncGroup from a path in a Store. @@ -3606,7 +3602,7 @@ async def _get_node_v2(store: Store, path: str) -> AsyncArray[ArrayV2Metadata] | return _build_node(store=store, path=path, metadata=metadata) -async def _get_node_v3(store: Store, path: str) -> AsyncArray[ArrayV3Metadata] | AsyncGroup: +async def _get_node_v3(store: Store, path: str) -> AsyncArrayV3 | AsyncGroup: """ Read a Zarr v3 AsyncArray or AsyncGroup from a path in a Store. diff --git a/src/zarr/types.py b/src/zarr/types.py index 2164710f1c..38990982f9 100644 --- a/src/zarr/types.py +++ b/src/zarr/types.py @@ -7,6 +7,11 @@ AnyAsyncArray: TypeAlias = AsyncArray[Any] """A Zarr format 2 or 3 `AsyncArray`""" +AsyncArrayV2: TypeAlias = AsyncArray[ArrayV2Metadata] +"""A Zarr format 2 `AsyncArray`""" + +AsyncArrayV3: TypeAlias = AsyncArray[ArrayV3Metadata] +"""A Zarr format 3 `AsyncArray`""" AnyArray: TypeAlias = Array[Any] """A Zarr format 2 or 3 `Array`""" From a04296cebbc6e81f5b10c0936be18c259d9ea727 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 28 Jul 2025 16:46:29 +0100 Subject: [PATCH 5/5] Add changelog entry --- changes/3304.feature.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes/3304.feature.rst diff --git a/changes/3304.feature.rst b/changes/3304.feature.rst new file mode 100644 index 0000000000..45dbd85731 --- /dev/null +++ b/changes/3304.feature.rst @@ -0,0 +1,2 @@ +The `Array` class can now also be parametrized in the same manner as the `AsyncArray` class, allowing Zarr format v2 and v3 `Array`s to be distinguished. +New types have been added to `zarr.types` to help with this.