Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4419.bugfix.md
Original file line number Diff line number Diff line change
@@ -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.
44 changes: 29 additions & 15 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Array,
AsyncArray,
CompressorLike,
_copy_from_array,
create_array,
from_array,
get_array_metadata,
Expand Down Expand Up @@ -612,32 +613,42 @@ 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
-------
array : array
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:
Expand All @@ -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

Expand Down
8 changes: 6 additions & 2 deletions src/zarr/api/synchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
32 changes: 19 additions & 13 deletions src/zarr/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
*,
Expand Down Expand Up @@ -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:
Expand Down
95 changes: 95 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading