From 7c5d804961ca4c364f88598ea92572a37707f9c7 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 25 Sep 2026 09:48:29 +0200 Subject: [PATCH 1/3] fix(group): create group helper arrays in the group's zarr format Group.empty/zeros/ones/full and their *_like versions forwarded **kwargs to the top-level creation functions without the group's zarr_format, so a zarr v2 group got zarr v3 arrays. The group lists only members of its own format, so each array was written into the group without becoming a member of it. Pass the group's format, and raise when the caller asks for another. Creating an array in one format like an array of the other then failed, because _like_args copied format-specific codec settings (compressor and filters, or codecs). _like_args now takes the target format and copies those settings only when the formats match. Top-level *_like functions inherit the source's format unless another is requested; open_like does not, because open_array would then look for an existing array in only that format. Assisted-by: ClaudeCode:claude-opus-5-5 --- src/zarr/api/asynchronous.py | 43 +++++++++++------ src/zarr/core/group.py | 71 ++++++++++++++++++++++++++--- tests/test_api.py | 55 ++++++++++++++++++++++ tests/test_api/test_asynchronous.py | 63 ++++++++++++++++--------- tests/test_group.py | 40 ++++++++++++++++ 5 files changed, 228 insertions(+), 44 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 1fc10cdd1e..742394d2c8 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -148,11 +148,18 @@ class _LikeArgs(TypedDict): compressor: NotRequired[CompressorLikev2] codecs: NotRequired[tuple[Codec, ...]] fill_value: NotRequired[Any] + zarr_format: NotRequired[ZarrFormat] -def _like_args(a: ArrayLike) -> _LikeArgs: - """Set default values for shape and chunks if they are not present in the array-like object""" +def _like_args(a: ArrayLike, zarr_format: ZarrFormat | None) -> _LikeArgs: + """ + Arguments for creating an array like `a` in `zarr_format`. + If `a` is a zarr array, the new array has the zarr format of `a` unless `zarr_format` + requests another. The storage settings of `a` (memory order and codecs) are specific to + its zarr format, so they are copied only when the new array has the format of `a`. + Otherwise the new array uses the defaults of its own format. + """ new: _LikeArgs = {} shape, chunks = _get_shape_chunks(a) @@ -166,14 +173,17 @@ def _like_args(a: ArrayLike) -> _LikeArgs: if isinstance(a, AsyncArray | Array): new["fill_value"] = a.metadata.fill_value - if isinstance(a.metadata, ArrayV2Metadata): - new["order"] = a.order - new["compressor"] = a.metadata.compressor - new["filters"] = a.metadata.filters - else: - # TODO: Remove type: ignore statement when type inference improves. - # mypy cannot correctly infer the type of a.metadata here for some reason. - new["codecs"] = a.metadata.codecs + if zarr_format is None: + zarr_format = new["zarr_format"] = a.metadata.zarr_format + if a.metadata.zarr_format == zarr_format: + if isinstance(a.metadata, ArrayV2Metadata): + new["order"] = a.order + new["compressor"] = a.metadata.compressor + new["filters"] = a.metadata.filters + else: + # TODO: Remove type: ignore statement when type inference improves. + # mypy cannot correctly infer the type of a.metadata here for some reason. + new["codecs"] = a.metadata.codecs else: # TODO: set default values compressor/codecs @@ -1141,7 +1151,7 @@ async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ - like_kwargs = _like_args(a) | kwargs + like_kwargs = _like_args(a, kwargs.get("zarr_format")) | kwargs return await empty(**like_kwargs) # type: ignore[arg-type] @@ -1183,7 +1193,7 @@ async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_kwargs = _like_args(a) | kwargs + like_kwargs = _like_args(a, kwargs.get("zarr_format")) | kwargs return await full(**like_kwargs) # type: ignore[arg-type] @@ -1221,7 +1231,7 @@ async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_args = _like_args(a) + like_args = _like_args(a, kwargs.get("zarr_format")) # `ones` supplies its own fill_value, so drop any inherited from `a`. like_args.pop("fill_value", None) like_kwargs = like_args | kwargs @@ -1301,7 +1311,10 @@ async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: AsyncArray The opened array. """ - like_kwargs = _like_args(a) | kwargs + # The zarr format of `a` is not inherited: `open_array` would then look for an existing + # array in only that format. + zarr_format = kwargs.get("zarr_format") or _default_zarr_format() + like_kwargs = _like_args(a, zarr_format) | kwargs if like_kwargs.get("mode") is None: like_kwargs["mode"] = "a" return await open_array(path=path, **like_kwargs) # type: ignore[arg-type] @@ -1341,7 +1354,7 @@ async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_args = _like_args(a) + like_args = _like_args(a, kwargs.get("zarr_format")) # `zeros` supplies its own fill_value, so drop any inherited from `a`. like_args.pop("fill_value", None) like_kwargs = like_args | kwargs diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index d734e6b7cd..e549596cdb 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -1619,6 +1619,20 @@ async def tree( raise NotImplementedError("'expand' is not yet implemented.") return await group_tree_async(self, max_depth=level, max_nodes=max_nodes, plain=plain) + def _member_zarr_format(self, zarr_format: ZarrFormat | None) -> ZarrFormat: + """ + The zarr format of a new array in this group, which is the format of the group. + + A group only lists members of its own format, so an array of another format + would be written into the group without becoming a member of it. + """ + if zarr_format is not None and zarr_format != self.metadata.zarr_format: + raise ValueError( + f"Cannot create a zarr_format={zarr_format} array in a " + f"zarr_format={self.metadata.zarr_format} group." + ) + return self.metadata.zarr_format + 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. @@ -1638,7 +1652,13 @@ async def empty(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> An retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ - return await async_api.empty(shape=shape, store=self.store_path, path=name, **kwargs) + return await async_api.empty( + shape=shape, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) 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. @@ -1657,7 +1677,13 @@ async def zeros(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> An AsyncArray The new array. """ - return await async_api.zeros(shape=shape, store=self.store_path, path=name, **kwargs) + return await async_api.zeros( + shape=shape, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) 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. @@ -1676,7 +1702,13 @@ async def ones(self, *, name: str, shape: tuple[int, ...], **kwargs: Any) -> Any AsyncArray The new array. """ - return await async_api.ones(shape=shape, store=self.store_path, path=name, **kwargs) + return await async_api.ones( + shape=shape, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) async def full( self, *, name: str, shape: tuple[int, ...], fill_value: Any | None, **kwargs: Any @@ -1704,6 +1736,7 @@ async def full( fill_value=fill_value, store=self.store_path, path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), **kwargs, ) @@ -1727,7 +1760,13 @@ async def empty_like( AsyncArray The new array. """ - return await async_api.empty_like(a=data, store=self.store_path, path=name, **kwargs) + return await async_api.empty_like( + a=data, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) async def zeros_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any @@ -1748,7 +1787,13 @@ async def zeros_like( AsyncArray The new array. """ - return await async_api.zeros_like(a=data, store=self.store_path, path=name, **kwargs) + return await async_api.zeros_like( + a=data, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) async def ones_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any @@ -1769,7 +1814,13 @@ async def ones_like( AsyncArray The new array. """ - return await async_api.ones_like(a=data, store=self.store_path, path=name, **kwargs) + return await async_api.ones_like( + a=data, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) async def full_like( self, *, name: str, data: async_api.ArrayLike, **kwargs: Any @@ -1790,7 +1841,13 @@ async def full_like( AsyncArray The new array. """ - return await async_api.full_like(a=data, store=self.store_path, path=name, **kwargs) + return await async_api.full_like( + a=data, + store=self.store_path, + path=name, + zarr_format=self._member_zarr_format(kwargs.pop("zarr_format", None)), + **kwargs, + ) async def move(self, source: str, dest: str) -> None: """Move a sub-group or sub-array from one path to another. diff --git a/tests/test_api.py b/tests/test_api.py index 45d0c0dee4..d3d6051a48 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -356,6 +356,61 @@ def test_array_open_array_not_found_sync() -> None: Array.open(store) +@pytest.mark.parametrize("func", [zarr.empty_like, zarr.zeros_like, zarr.ones_like, zarr.full_like]) +@pytest.mark.parametrize("source_format", [2, 3]) +@pytest.mark.parametrize("zarr_format", [None, 2, 3]) +def test_like_zarr_format( + func: Callable[..., AnyArray], source_format: ZarrFormat, zarr_format: ZarrFormat | None +) -> None: + """ + An array created like a zarr array has the zarr format of the source unless another is + requested, and keeps the codecs of the source only when the formats match. + """ + source = zarr.create_array( + {}, + shape=(4,), + dtype="int32", + compressors=None, + zarr_format=source_format, + fill_value=7, + ) + kwargs = {} if zarr_format is None else {"zarr_format": zarr_format} + new = func(source, **kwargs) + expected_format = source_format if zarr_format is None else zarr_format + assert new.metadata.zarr_format == expected_format + assert new.shape == source.shape + assert new.dtype == source.dtype + if expected_format == source_format: + assert new.compressors == source.compressors + else: + assert ( + new.compressors + == zarr.create_array( + {}, shape=(4,), dtype="int32", zarr_format=expected_format + ).compressors + ) + + +@pytest.mark.parametrize("source_format", [2, 3]) +@pytest.mark.parametrize("target_format", [None, 2, 3]) +def test_open_like_zarr_format(source_format: ZarrFormat, target_format: ZarrFormat | None) -> None: + """ + open_like does not inherit the zarr format of the source: it opens an existing array of + any format, and creates a missing one in the default zarr format. + """ + source = zarr.zeros(store={}, shape=(4,), dtype="int32", zarr_format=source_format) + store = MemoryStore() + if target_format is not None: + zarr.create_array( + store, name="existing", shape=(4,), dtype="int32", zarr_format=target_format + ) + opened = zarr.open_like(source, path="existing", store=store) + assert opened.metadata.zarr_format == (target_format or zarr.config.get("default_zarr_format")) + assert set(store._store_dict) & {"existing/zarr.json", "existing/.zarray"} == { + "existing/zarr.json" if opened.metadata.zarr_format == 3 else "existing/.zarray" + } + + @pytest.mark.parametrize("store", ["memory", "local", "zip"], indirect=True) def test_v2_and_v3_exist_at_same_path(store: Store) -> None: zarr.create_array(store, shape=(10,), dtype="uint8", zarr_format=3) diff --git a/tests/test_api/test_asynchronous.py b/tests/test_api/test_asynchronous.py index 6ebec36bbd..f0caf6e030 100644 --- a/tests/test_api/test_asynchronous.py +++ b/tests/test_api/test_asynchronous.py @@ -19,6 +19,7 @@ import numpy.typing as npt from zarr.core.array import AsyncArray + from zarr.core.common import ZarrFormat from zarr.core.metadata import ArrayV2Metadata, ArrayV3Metadata from zarr.types import AnyArray @@ -56,31 +57,48 @@ def test_get_shape_chunks( assert _get_shape_chunks(observed) == expected +_V2_ARRAY = create_array( + {}, + chunks=(10,), + shape=(100,), + dtype="f8", + compressors=None, + filters=None, + zarr_format=2, +)._async_array +_V2_ARRAY_ARGS = { + "chunks": (10,), + "shape": (100,), + "dtype": np.dtype("f8"), + "fill_value": np.float64(0.0), +} + + @pytest.mark.parametrize( - ("observed", "expected"), + ("observed", "zarr_format", "expected"), [ - (np.arange(10, dtype=np.dtype("int64")), {"shape": (10,), "dtype": np.dtype("int64")}), - (WithChunks(shape=(1, 2), chunks=(1, 2)), {"chunks": (1, 2), "shape": (1, 2)}), ( - create_array( - {}, - chunks=(10,), - shape=(100,), - dtype="f8", - compressors=None, - filters=None, - zarr_format=2, - )._async_array, - { - "chunks": (10,), - "shape": (100,), - "dtype": np.dtype("f8"), - "fill_value": np.float64(0.0), - "compressor": None, - "filters": None, - "order": "C", - }, + np.arange(10, dtype=np.dtype("int64")), + None, + {"shape": (10,), "dtype": np.dtype("int64")}, + ), + ( + np.arange(10, dtype=np.dtype("int64")), + 2, + {"shape": (10,), "dtype": np.dtype("int64")}, + ), + (WithChunks(shape=(1, 2), chunks=(1, 2)), None, {"chunks": (1, 2), "shape": (1, 2)}), + ( + _V2_ARRAY, + None, + _V2_ARRAY_ARGS | {"zarr_format": 2, "compressor": None, "filters": None, "order": "C"}, + ), + ( + _V2_ARRAY, + 2, + _V2_ARRAY_ARGS | {"compressor": None, "filters": None, "order": "C"}, ), + (_V2_ARRAY, 3, _V2_ARRAY_ARGS), ], ) def test_like_args( @@ -88,12 +106,13 @@ def test_like_args( | AsyncArray[ArrayV3Metadata] | AnyArray | npt.NDArray[Any], + zarr_format: ZarrFormat | None, expected: object, ) -> None: """ Test the like_args function """ - assert _like_args(observed) == expected + assert _like_args(observed, zarr_format) == expected async def test_open_no_array() -> None: diff --git a/tests/test_group.py b/tests/test_group.py index 31fbd138cd..2e011e5f7e 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -908,6 +908,46 @@ def test_group_array_like_creation( assert np.all(new_arr[:] == expect_fill) +@pytest.mark.parametrize("group_format", [2, 3]) +@pytest.mark.parametrize( + "method_name", + ["empty", "zeros", "ones", "full", "empty_like", "zeros_like", "ones_like", "full_like"], +) +@pytest.mark.parametrize("source_format", [2, 3]) +@pytest.mark.parametrize("pass_zarr_format", [False, True]) +def test_group_array_helpers_use_group_format( + group_format: ZarrFormat, + method_name: str, + source_format: ZarrFormat, + pass_zarr_format: bool, +) -> None: + """ + Group.{empty, zeros, ones, full} and their *_like versions create an array in the zarr + format of the group, which makes it a member of the group. For the *_like versions this + holds whatever the format of the source array. + """ + group = Group.from_store(MemoryStore(), zarr_format=group_format) + kwargs: dict[str, Any] = {"zarr_format": group_format} if pass_zarr_format else {} + if method_name == "full": + kwargs["fill_value"] = 3 + if method_name.endswith("_like"): + source = zarr.zeros(store={}, shape=(4,), dtype="int32", zarr_format=source_format) + arr = getattr(group, method_name)(name="a", data=source, **kwargs) + else: + arr = getattr(group, method_name)(name="a", shape=(4,), **kwargs) + assert arr.metadata.zarr_format == group_format + assert list(group.array_keys()) == ["a"] + + +def test_group_array_helpers_other_format() -> None: + """ + Asking a group helper for an array in a zarr format other than the group's raises. + """ + group = Group.from_store(MemoryStore(), zarr_format=2) + with pytest.raises(ValueError, match="zarr_format=3 array in a zarr_format=2 group"): + group.zeros(name="a", shape=(4,), zarr_format=3) + + def test_group_array_creation( store: Store, zarr_format: ZarrFormat, From c2417bdc160ea6004c6ca8e5dc440d2858c5c839 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Fri, 25 Sep 2026 09:49:00 +0200 Subject: [PATCH 2/3] docs(changes): add fragment for #4412 Assisted-by: ClaudeCode:claude-opus-5-5 --- changes/4412.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/4412.bugfix.md diff --git a/changes/4412.bugfix.md b/changes/4412.bugfix.md new file mode 100644 index 0000000000..3fefca87cf --- /dev/null +++ b/changes/4412.bugfix.md @@ -0,0 +1 @@ +`Group.empty`, `zeros`, `ones`, `full` and their `*_like` versions now create arrays in the zarr format of the group. Previously a zarr v2 group got zarr v3 arrays, which were written into the group without becoming members of it. Creating an array like an array of the other zarr format (for example `zarr.zeros_like(v2_array)`) no longer fails. The new array uses the default codecs of its own format, and top-level `*_like` functions inherit the zarr format of the source array unless `zarr_format` is passed. From 02898a2d391f841ba1819142084ab0122ae5e021 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Wed, 30 Sep 2026 14:56:22 +0200 Subject: [PATCH 3/3] fix(api): take zarr_format as a named parameter of the *_like functions `empty_like`, `zeros_like`, `ones_like`, `full_like` and `open_like` read the target format with `kwargs.get("zarr_format")` and then merged `kwargs` over the arguments derived from the source array. An explicit `zarr_format=None` therefore made `_like_args` inherit the source's format and copy its codecs, after which the merge put `None` back and `create` chose the default format: `zarr.zeros_like(v2_array, zarr_format=None)` raised because v2 compressor settings reached a v3 array. `zarr_format` is now a keyword-only parameter, and `_like_args` records the resolved format in the arguments it returns. Assisted-by: ClaudeCode:claude-opus-5-5 --- src/zarr/api/asynchronous.py | 56 ++++++++++++++++++++++------- src/zarr/api/synchronous.py | 38 ++++++++++++++------ tests/test_api.py | 3 +- tests/test_api/test_asynchronous.py | 6 ++-- 4 files changed, 75 insertions(+), 28 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 8ba4b00412..9f49e8a9b9 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -174,7 +174,7 @@ def _like_args(a: ArrayLike, zarr_format: ZarrFormat | None) -> _LikeArgs: if isinstance(a, AsyncArray | Array): new["fill_value"] = a.metadata.fill_value if zarr_format is None: - zarr_format = new["zarr_format"] = a.metadata.zarr_format + zarr_format = a.metadata.zarr_format if a.metadata.zarr_format == zarr_format: if isinstance(a.metadata, ArrayV2Metadata): new["order"] = a.order @@ -191,6 +191,9 @@ def _like_args(a: ArrayLike, zarr_format: ZarrFormat | None) -> _LikeArgs: # new["compressor"] = "default" pass + if zarr_format is not None: + new["zarr_format"] = zarr_format + return new @@ -1136,7 +1139,9 @@ async def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: return await create(shape=shape, **kwargs) -async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: +async def empty_like( + a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **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. @@ -1144,6 +1149,9 @@ async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. @@ -1158,7 +1166,7 @@ async def empty_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ - like_kwargs = _like_args(a, kwargs.get("zarr_format")) | kwargs + like_kwargs = _like_args(a, zarr_format) | kwargs return await empty(**like_kwargs) # type: ignore[arg-type] @@ -1185,13 +1193,18 @@ async def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyAsy # TODO: add type annotations for kwargs -async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: +async def full_like( + a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any +) -> AnyAsyncArray: """Create a filled array like `a`. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. @@ -1200,7 +1213,7 @@ async def full_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_kwargs = _like_args(a, kwargs.get("zarr_format")) | kwargs + like_kwargs = _like_args(a, zarr_format) | kwargs return await full(**like_kwargs) # type: ignore[arg-type] @@ -1223,13 +1236,18 @@ async def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: return await create(shape=shape, fill_value=1, **kwargs) -async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: +async def ones_like( + a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any +) -> AnyAsyncArray: """Create an array of ones like `a`. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. @@ -1238,7 +1256,7 @@ async def ones_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_args = _like_args(a, kwargs.get("zarr_format")) + like_args = _like_args(a, zarr_format) # `ones` supplies its own fill_value, so drop any inherited from `a`. like_args.pop("fill_value", None) like_kwargs = like_args | kwargs @@ -1299,7 +1317,9 @@ async def open_array( raise ArrayNotFoundError(msg) from err -async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: +async def open_like( + a: ArrayLike, path: str, *, zarr_format: ZarrFormat | None = None, **kwargs: Any +) -> AnyAsyncArray: """Open a persistent array like `a`. Parameters @@ -1308,6 +1328,10 @@ async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: The shape and data-type of a define these same attributes of the returned array. path : str The path to the new array. + zarr_format : {2, 3, None}, optional + The zarr format of the array to open or create. If `None` (default), an existing + array of either format is opened, and a missing one is created in the default zarr + format. The zarr format of `a` is not inherited. **kwargs Additional keyword arguments passed to `open_array`. If `mode` is omitted or `None`, it defaults to `"a"`. Pass `mode="r"` when @@ -1320,11 +1344,12 @@ async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyAsyncArray: """ # The zarr format of `a` is not inherited: `open_array` would then look for an existing # array in only that format. - zarr_format = kwargs.get("zarr_format") or _default_zarr_format() - like_kwargs = _like_args(a, zarr_format) | kwargs + like_args = _like_args(a, zarr_format or _default_zarr_format()) + like_args.pop("zarr_format") + like_kwargs = like_args | kwargs if like_kwargs.get("mode") is None: like_kwargs["mode"] = "a" - return await open_array(path=path, **like_kwargs) # type: ignore[arg-type] + return await open_array(path=path, zarr_format=zarr_format, **like_kwargs) # type: ignore[arg-type] async def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: @@ -1346,13 +1371,18 @@ async def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyAsyncArray: return await create(shape=shape, fill_value=0, **kwargs) -async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: +async def zeros_like( + a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any +) -> AnyAsyncArray: """Create an array of zeros like `a`. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. @@ -1361,7 +1391,7 @@ async def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyAsyncArray: Array The new array. """ - like_args = _like_args(a, kwargs.get("zarr_format")) + like_args = _like_args(a, zarr_format) # `zeros` supplies its own fill_value, so drop any inherited from `a`. like_args.pop("fill_value", None) like_kwargs = like_args | kwargs diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 13bdf2a651..e4b149ec65 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -1257,7 +1257,7 @@ def empty(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs -def empty_like(a: ArrayLike, **kwargs: Any) -> AnyArray: +def empty_like(a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **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. @@ -1265,6 +1265,9 @@ def empty_like(a: ArrayLike, **kwargs: Any) -> AnyArray: ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. @@ -1279,7 +1282,7 @@ def empty_like(a: ArrayLike, **kwargs: Any) -> AnyArray: retrieve data from an empty Zarr array, any values may be returned, and these are not guaranteed to be stable from one access to the next. """ - return Array(sync(async_api.empty_like(a, **kwargs))) + return Array(sync(async_api.empty_like(a, zarr_format=zarr_format, **kwargs))) # TODO: add type annotations for kwargs and fill_value @@ -1305,13 +1308,16 @@ def full(shape: tuple[int, ...], fill_value: Any, **kwargs: Any) -> AnyArray: # TODO: move ArrayLike to common module # TODO: add type annotations for kwargs -def full_like(a: ArrayLike, **kwargs: Any) -> AnyArray: +def full_like(a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any) -> AnyArray: """Create a filled array like another array. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. @@ -1320,7 +1326,7 @@ def full_like(a: ArrayLike, **kwargs: Any) -> AnyArray: Array The new array. """ - return Array(sync(async_api.full_like(a, **kwargs))) + return Array(sync(async_api.full_like(a, zarr_format=zarr_format, **kwargs))) # TODO: add type annotations for kwargs @@ -1343,13 +1349,16 @@ def ones(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: # TODO: add type annotations for kwargs -def ones_like(a: ArrayLike, **kwargs: Any) -> AnyArray: +def ones_like(a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any) -> AnyArray: """Create an array of ones like another array. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`zarr.api.asynchronous.create`][]. @@ -1358,7 +1367,7 @@ def ones_like(a: ArrayLike, **kwargs: Any) -> AnyArray: Array The new array. """ - return Array(sync(async_api.ones_like(a, **kwargs))) + return Array(sync(async_api.ones_like(a, zarr_format=zarr_format, **kwargs))) # TODO: update this once async_api.open_array is fully implemented @@ -1408,7 +1417,9 @@ def open_array( # TODO: add type annotations for kwargs -def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyArray: +def open_like( + a: ArrayLike, path: str, *, zarr_format: ZarrFormat | None = None, **kwargs: Any +) -> AnyArray: """Open a persistent array like another array. Parameters @@ -1417,6 +1428,10 @@ def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyArray: The shape and data-type of a define these same attributes of the returned array. path : str The path to the new array. + zarr_format : {2, 3, None}, optional + The zarr format of the array to open or create. If `None` (default), an existing + array of either format is opened, and a missing one is created in the default zarr + format. The zarr format of `a` is not inherited. **kwargs Additional keyword arguments passed to `open_array`. If `mode` is omitted or `None`, it defaults to `"a"`. Pass `mode="r"` when @@ -1427,7 +1442,7 @@ def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AnyArray: Array The opened array. """ - return Array(sync(async_api.open_like(a, path=path, **kwargs))) + return Array(sync(async_api.open_like(a, path=path, zarr_format=zarr_format, **kwargs))) # TODO: add type annotations for kwargs @@ -1450,13 +1465,16 @@ def zeros(shape: tuple[int, ...], **kwargs: Any) -> AnyArray: # TODO: add type annotations for kwargs -def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyArray: +def zeros_like(a: ArrayLike, *, zarr_format: ZarrFormat | None = None, **kwargs: Any) -> AnyArray: """Create an array of zeros like another array. Parameters ---------- a : array-like The array to create an empty array like. + zarr_format : {2, 3, None}, optional + The zarr format of the new array. If `None` (default), the zarr format of `a` if it + is a zarr array, otherwise the default zarr format. **kwargs Keyword arguments passed to [`create`][zarr.api.asynchronous.create]. @@ -1465,4 +1483,4 @@ def zeros_like(a: ArrayLike, **kwargs: Any) -> AnyArray: Array The new array. """ - return Array(sync(async_api.zeros_like(a, **kwargs))) + return Array(sync(async_api.zeros_like(a, zarr_format=zarr_format, **kwargs))) diff --git a/tests/test_api.py b/tests/test_api.py index 52a9069176..96bbd26d26 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -375,8 +375,7 @@ def test_like_zarr_format( zarr_format=source_format, fill_value=7, ) - kwargs = {} if zarr_format is None else {"zarr_format": zarr_format} - new = func(source, **kwargs) + new = func(source, zarr_format=zarr_format) expected_format = source_format if zarr_format is None else zarr_format assert new.metadata.zarr_format == expected_format assert new.shape == source.shape diff --git a/tests/test_api/test_asynchronous.py b/tests/test_api/test_asynchronous.py index f0caf6e030..66fd6819dd 100644 --- a/tests/test_api/test_asynchronous.py +++ b/tests/test_api/test_asynchronous.py @@ -85,7 +85,7 @@ def test_get_shape_chunks( ( np.arange(10, dtype=np.dtype("int64")), 2, - {"shape": (10,), "dtype": np.dtype("int64")}, + {"shape": (10,), "dtype": np.dtype("int64"), "zarr_format": 2}, ), (WithChunks(shape=(1, 2), chunks=(1, 2)), None, {"chunks": (1, 2), "shape": (1, 2)}), ( @@ -96,9 +96,9 @@ def test_get_shape_chunks( ( _V2_ARRAY, 2, - _V2_ARRAY_ARGS | {"compressor": None, "filters": None, "order": "C"}, + _V2_ARRAY_ARGS | {"zarr_format": 2, "compressor": None, "filters": None, "order": "C"}, ), - (_V2_ARRAY, 3, _V2_ARRAY_ARGS), + (_V2_ARRAY, 3, _V2_ARRAY_ARGS | {"zarr_format": 3}), ], ) def test_like_args(