From 7e9e370e92f3bddd969161489939839112090a7a Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sat, 26 Sep 2026 15:57:43 +0200 Subject: [PATCH 1/2] fix(api): accept create keyword arguments when zarr.array copies a zarr array zarr.array documents its keyword arguments as passed through to create, but a zarr Array as data was sent to from_array instead, which requires store and takes a different set of arguments. zarr.array(zarr_array) raised "missing store", and create arguments such as compressor, mode or read_only raised TypeError, while the same call with NumPy data worked. A zarr Array now takes the create path like any other data. Its zarr dtype (not the native dtype, which cannot identify string types), shape and chunks are the defaults, and a rectilinear grid is kept through read_chunk_sizes. The data is still streamed one shard region at a time by _copy_from_array, factored out of from_array. Assisted-by: ClaudeCode:claude-opus-5-5 --- src/zarr/api/asynchronous.py | 44 +++++++++++------ src/zarr/api/synchronous.py | 8 ++- src/zarr/core/array.py | 32 +++++++----- tests/test_api.py | 95 ++++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 30 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 1fc10cdd1e..c030a525c9 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -15,6 +15,7 @@ Array, AsyncArray, CompressorLike, + _copy_from_array, create_array, from_array, get_array_metadata, @@ -612,9 +613,13 @@ async def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyAsyncArray: Parameters ---------- data : array_like - The data to fill the array with. + The data to fill the array with. A Zarr array is copied one region at a time rather + than read into memory. **kwargs - Passed through to [`create`][zarr.api.asynchronous.create]. + Passed through to [`create`][zarr.api.asynchronous.create]. The shape of the new array + is that of `data`, and its dtype and chunks default to those of `data`. To also copy + the codecs and other settings of a Zarr array, use + [`from_array`][zarr.api.asynchronous.from_array]. Returns ------- @@ -622,22 +627,28 @@ async def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyAsyncArray: The new array. """ + data_shape: tuple[int, ...] | None + data_chunks: tuple[int, ...] | tuple[tuple[int, ...], ...] | None if isinstance(data, Array): - return await from_array(data=data, **kwargs) - - # ensure data is array-like - if not hasattr(data, "shape") or not hasattr(data, "dtype"): - data = np.asanyarray(data) - - # setup dtype - kw_dtype = kwargs.get("dtype") - if kw_dtype is None and hasattr(data, "dtype"): - kwargs["dtype"] = data.dtype + # A native object dtype cannot identify the source's Zarr data type. + data_dtype: ZDTypeLike | None = data._async_array._zdtype + data_shape = data.shape + try: + data_chunks = data.chunks + except NotImplementedError: + # `chunks` is undefined for an unsharded rectilinear chunk grid + data_chunks = data.read_chunk_sizes else: - kwargs["dtype"] = kw_dtype + # ensure data is array-like + if not hasattr(data, "shape") or not hasattr(data, "dtype"): + data = np.asanyarray(data) + data_dtype = data.dtype if hasattr(data, "dtype") else None + data_shape, data_chunks = _get_shape_chunks(data) + + if kwargs.get("dtype") is None: + kwargs["dtype"] = data_dtype # setup shape and chunks - data_shape, data_chunks = _get_shape_chunks(data) kwargs["shape"] = data_shape kw_chunks = kwargs.get("chunks") if kw_chunks is None: @@ -653,7 +664,10 @@ async def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyAsyncArray: z = await create(**kwargs) # fill with data - await z.setitem(Ellipsis, data) + if isinstance(data, Array): + await _copy_from_array(z, data) + else: + await z.setitem(Ellipsis, data) return z diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 6975f6d953..b81939fc7a 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -400,9 +400,13 @@ def array(data: npt.ArrayLike | AnyArray, **kwargs: Any) -> AnyArray: Parameters ---------- data : array_like - The data to fill the array with. + The data to fill the array with. A Zarr array is copied one region at a time rather + than read into memory. **kwargs - Passed through to [`create`][zarr.api.asynchronous.create]. + Passed through to [`create`][zarr.api.asynchronous.create]. The shape of the new array + is that of `data`, and its dtype and chunks default to those of `data`. To also copy + the codecs and other settings of a Zarr array, use + [`from_array`][zarr.api.asynchronous.from_array]. Returns ------- diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 5a8d6bf57e..7a56eaf454 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -4132,6 +4132,24 @@ class ShardsConfigParam(TypedDict): ) +async def _copy_from_array(result: AnyAsyncArray, data: AnyArray) -> None: + """ + Stream the data of `data` into `result`, one shard region of `result` at a time. + + `result` must have the shape of `data`. + """ + + async def _copy_array_region(chunk_coords: tuple[int, ...] | slice, _data: AnyArray) -> None: + arr = await _data.async_array.getitem(chunk_coords) + await result.setitem(chunk_coords, arr) + + await concurrent_map( + [(region, data) for region in result._iter_shard_regions()], + _copy_array_region, + zarr.core.config.config.get("async.concurrency"), + ) + + async def from_array( store: StoreLike, *, @@ -4389,19 +4407,7 @@ async def from_array( if write_data: if isinstance(data, Array): - - async def _copy_array_region( - chunk_coords: tuple[int, ...] | slice, _data: AnyArray - ) -> None: - arr = await _data.async_array.getitem(chunk_coords) - await result.setitem(chunk_coords, arr) - - # Stream data from the source array to the new array - await concurrent_map( - [(region, data) for region in result._iter_shard_regions()], - _copy_array_region, - zarr.core.config.config.get("async.concurrency"), - ) + await _copy_from_array(result, data) else: async def _copy_arraylike_region(chunk_coords: slice, _data: NDArrayLike) -> None: diff --git a/tests/test_api.py b/tests/test_api.py index 45d0c0dee4..68da1d1c5d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -356,6 +356,101 @@ def test_array_open_array_not_found_sync() -> None: Array.open(store) +def _zarr_source(kind: str) -> AnyArray: + """A small zarr array of the given kind, filled with data.""" + if kind == "rectilinear": + with zarr.config.set({"array.rectilinear_chunks": True}): + src = zarr.create_array({}, shape=(6,), chunks=[[2, 4]], dtype="int32") + elif kind == "string": + src = zarr.create_array({}, shape=(6,), chunks=(2,), dtype="str") + src[:] = np.array(list("abcdef"), dtype=object) + return src + elif kind == "sharded": + src = zarr.create_array({}, shape=(6,), chunks=(1,), shards=(2,), dtype="int32") + else: + src = zarr.create_array( + {}, shape=(6,), chunks=(2,), dtype="int32", zarr_format=2 if kind == "v2" else 3 + ) + src[:] = np.arange(6, dtype="int32") + return src + + +_ARRAY_KWARGS = { + "none": {}, + "store": {"store": "memory"}, + "store-path-mode-attributes": { + "store": "memory", + "path": "a", + "mode": "w", + "attributes": {"x": 1}, + }, + "chunks": {"chunks": (3,)}, + "zarr_format": {"zarr_format": 2}, + "fill_value": {"fill_value": 5}, +} + + +@pytest.mark.parametrize( + ("kind", "kwargs"), + [ + pytest.param(kind, kwargs, id=f"{kind}-{name}") + for kind in ["v2", "v3", "sharded", "rectilinear", "string"] + for name, kwargs in _ARRAY_KWARGS.items() + # zarr format 2 has no rectilinear chunk grids + if not (kind == "rectilinear" and name == "zarr_format") + ], +) +def test_array_from_zarr_array(kind: str, kwargs: dict[str, Any]) -> None: + """ + zarr.array accepts the same keyword arguments for a zarr array as for any other data: + the new array has the shape and data of the source, dtype and chunks default to those + of the source, and the rest are the arguments to `create`. + """ + src = _zarr_source(kind) + if kwargs.get("store") == "memory": + kwargs = kwargs | {"store": MemoryStore()} + with zarr.config.set({"array.rectilinear_chunks": True}): + new = zarr.array(src, **kwargs) + assert new.shape == src.shape + assert_array_equal(new[:], src[:]) + assert new._async_array._zdtype == src._async_array._zdtype + if "chunks" in kwargs: + assert new.chunks == kwargs["chunks"] + else: + # the new array is not sharded, so its chunks are the inner chunks of the source + assert new.read_chunk_sizes == ( + src.read_chunk_sizes + if kind == "rectilinear" + else ((src.chunks[0],) * (src.shape[0] // src.chunks[0]),) + ) + assert new.metadata.zarr_format == kwargs.get("zarr_format", 3) + assert new.attrs == kwargs.get("attributes", {}) + if "fill_value" in kwargs: + assert new.fill_value == new._async_array._zdtype.cast_scalar(kwargs["fill_value"]) + if "path" in kwargs: + assert new.path == kwargs["path"] + + +def test_array_from_zarr_array_rectilinear_v2() -> None: + """ + zarr.array raises for a zarr format 2 copy of a rectilinear zarr array, whose chunks + format 2 cannot represent. + """ + with ( + zarr.config.set({"array.rectilinear_chunks": True}), + pytest.raises(ValueError, match="Zarr format 2 does not support rectilinear chunk grids"), + ): + zarr.array(_zarr_source("rectilinear"), zarr_format=2) + + +def test_array_from_zarr_array_read_only() -> None: + """ + zarr.array raises for read_only=True with a zarr array, as it does for other data. + """ + with pytest.raises(ValueError, match="read_only=True is no longer supported"): + zarr.array(_zarr_source("v3"), read_only=True) + + @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) From 95513ca044f77b8358c50ab2c34cf53a79ec0306 Mon Sep 17 00:00:00 2001 From: Davis Vann Bennett Date: Sat, 26 Sep 2026 15:59:00 +0200 Subject: [PATCH 2/2] docs(changes): add fragment for #4419 Assisted-by: ClaudeCode:claude-opus-5-5 --- changes/4419.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/4419.bugfix.md diff --git a/changes/4419.bugfix.md b/changes/4419.bugfix.md new file mode 100644 index 0000000000..0c8d21d503 --- /dev/null +++ b/changes/4419.bugfix.md @@ -0,0 +1 @@ +`zarr.array` now accepts the same keyword arguments for a zarr array as for any other data, as its documentation says. Previously a zarr array was passed to `from_array`, so `zarr.array(zarr_array)` raised for a missing `store`, and `create` arguments such as `mode` or `dtype` raised `TypeError`. The new array takes its shape, dtype and chunks from the source by default, and the data is still copied one region at a time. The source's codecs, fill value and attributes are no longer copied; use `zarr.from_array` for that.