Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5ddd68c
ensure that user-provided array creation kwargs can pass through arra…
d-v-b Apr 17, 2025
61f7008
test for kwarg propagation through array-like routines
d-v-b Apr 17, 2025
6328336
propagate fill value if unspecified
d-v-b Apr 17, 2025
71e76a0
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b Apr 17, 2025
aac415e
changelog
d-v-b Apr 17, 2025
9d419eb
Merge branch 'fix/zeros-like-takes-kwargs' of https://github.com/d-v-…
d-v-b Apr 17, 2025
98feed3
Update 2992.fix.rst
d-v-b Apr 17, 2025
c3149a6
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b Apr 18, 2025
cf8c4e3
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b Apr 19, 2025
94294dc
add test for open_like
d-v-b Apr 19, 2025
cb132bd
Update 2992.fix.rst
d-v-b Apr 19, 2025
aa129cc
lint
d-v-b Apr 19, 2025
26ad110
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b Apr 28, 2025
7b8b0f9
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b May 9, 2025
cb99615
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b May 23, 2025
3893e55
add likeargs typeddict
d-v-b May 23, 2025
19b1ce2
explicitly iterate over functions in test
d-v-b May 23, 2025
d29f682
add test cases for fill_value in test_array_like_creation
d-v-b May 23, 2025
36bda1d
use correct type: ignore statement
d-v-b May 23, 2025
9a0b8f6
Merge branch 'main' of https://github.com/zarr-developers/zarr-python…
d-v-b Jun 26, 2025
71259b3
remove test that made no sense after allowing dtype inference in full…
d-v-b Jun 26, 2025
04593bd
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b Jun 27, 2025
ac9682a
Merge branch 'main' into fix/zeros-like-takes-kwargs
d-v-b Sep 17, 2025
b246af4
avoid inserting order for v3 arrays in _like_args
d-v-b Sep 17, 2025
4186442
fix change note
d-v-b Sep 17, 2025
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
3 changes: 3 additions & 0 deletions changes/2992.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a bug preventing ``ones_like``, ``full_like``, ``empty_like``, ``zeros_like`` and ``open_like`` functions from accepting
an explicit specification of array attributes like shape, dtype, chunks etc. The functions ``full_like``,
``empty_like``, and ``open_like`` now also more consistently infer a ``fill_value`` parameter from the provided array.
52 changes: 32 additions & 20 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, NotRequired, TypedDict, cast

import numpy as np
import numpy.typing as npt
Expand Down Expand Up @@ -56,6 +56,7 @@
from zarr.abc.numcodec import Numcodec
from zarr.core.buffer import NDArrayLikeOrScalar
from zarr.core.chunk_key_encodings import ChunkKeyEncoding
from zarr.core.metadata.v2 import CompressorLikev2
from zarr.storage import StoreLike

# TODO: this type could use some more thought
Expand Down Expand Up @@ -124,10 +125,20 @@ def _get_shape_chunks(a: ArrayLike | Any) -> tuple[tuple[int, ...] | None, tuple
return shape, chunks


def _like_args(a: ArrayLike, kwargs: dict[str, Any]) -> dict[str, Any]:
class _LikeArgs(TypedDict):
shape: NotRequired[tuple[int, ...]]
chunks: NotRequired[tuple[int, ...]]
dtype: NotRequired[np.dtype[np.generic]]
order: NotRequired[Literal["C", "F"]]
filters: NotRequired[tuple[Numcodec, ...] | None]
compressor: NotRequired[CompressorLikev2]
codecs: NotRequired[tuple[Codec, ...]]


def _like_args(a: ArrayLike) -> _LikeArgs:
"""Set default values for shape and chunks if they are not present in the array-like object"""

new = kwargs.copy()
new: _LikeArgs = {}

shape, chunks = _get_shape_chunks(a)
if shape is not None:
Expand All @@ -138,9 +149,9 @@ def _like_args(a: ArrayLike, kwargs: dict[str, Any]) -> dict[str, Any]:
if hasattr(a, "dtype"):
new["dtype"] = a.dtype

if isinstance(a, AsyncArray):
new["order"] = a.order
if isinstance(a, AsyncArray | Array):
if isinstance(a.metadata, ArrayV2Metadata):
new["order"] = a.order
new["compressor"] = a.metadata.compressor
new["filters"] = a.metadata.filters
else:
Expand Down Expand Up @@ -1087,7 +1098,7 @@ async def empty(
shape: tuple[int, ...], **kwargs: Any
) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata]:
"""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.
specified fill value or zeros if no fill value is provided.

Parameters
----------
Expand All @@ -1102,8 +1113,7 @@ async def empty(
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 create(shape=shape, fill_value=None, **kwargs)
return await create(shape=shape, **kwargs)


async def empty_like(
Expand All @@ -1130,8 +1140,10 @@ async def empty_like(
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)
return await empty(**like_kwargs)
like_kwargs = _like_args(a) | kwargs
if isinstance(a, (AsyncArray | Array)):
like_kwargs.setdefault("fill_value", a.metadata.fill_value)
return await empty(**like_kwargs) # type: ignore[arg-type]


# TODO: add type annotations for fill_value and kwargs
Expand Down Expand Up @@ -1176,10 +1188,10 @@ async def full_like(
Array
The new array.
"""
like_kwargs = _like_args(a, kwargs)
if isinstance(a, AsyncArray):
like_kwargs = _like_args(a) | kwargs
if isinstance(a, (AsyncArray | Array)):
like_kwargs.setdefault("fill_value", a.metadata.fill_value)
return await full(**like_kwargs)
return await full(**like_kwargs) # type: ignore[arg-type]


async def ones(
Expand Down Expand Up @@ -1220,8 +1232,8 @@ async def ones_like(
Array
The new array.
"""
like_kwargs = _like_args(a, kwargs)
return await ones(**like_kwargs)
like_kwargs = _like_args(a) | kwargs
return await ones(**like_kwargs) # type: ignore[arg-type]


async def open_array(
Expand Down Expand Up @@ -1300,10 +1312,10 @@ async def open_like(
AsyncArray
The opened array.
"""
like_kwargs = _like_args(a, kwargs)
like_kwargs = _like_args(a) | kwargs
if isinstance(a, (AsyncArray | Array)):
kwargs.setdefault("fill_value", a.metadata.fill_value)
return await open_array(path=path, **like_kwargs)
like_kwargs.setdefault("fill_value", a.metadata.fill_value)
return await open_array(path=path, **like_kwargs) # type: ignore[arg-type]


async def zeros(
Expand Down Expand Up @@ -1344,5 +1356,5 @@ async def zeros_like(
Array
The new array.
"""
like_kwargs = _like_args(a, kwargs)
return await zeros(**like_kwargs)
like_kwargs = _like_args(a) | kwargs
return await zeros(**like_kwargs) # type: ignore[arg-type]
87 changes: 86 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import inspect
import re
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any

import zarr.codecs
import zarr.storage
Expand Down Expand Up @@ -82,6 +82,91 @@ def test_create(memory_store: Store) -> None:
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type]


@pytest.mark.parametrize(
"func",
[
zarr.api.asynchronous.zeros_like,
zarr.api.asynchronous.ones_like,
zarr.api.asynchronous.empty_like,
zarr.api.asynchronous.full_like,
zarr.api.asynchronous.open_like,
],
)
@pytest.mark.parametrize("out_shape", ["keep", (10, 10)])
@pytest.mark.parametrize("out_chunks", ["keep", (10, 10)])
@pytest.mark.parametrize("out_dtype", ["keep", "int8"])
@pytest.mark.parametrize("out_fill", ["keep", 4])
async def test_array_like_creation(
zarr_format: ZarrFormat,
func: Callable[[Any], Any],
out_shape: Literal["keep"] | tuple[int, ...],
out_chunks: Literal["keep"] | tuple[int, ...],
out_dtype: str,
out_fill: Literal["keep"] | int,
) -> None:
"""
Test zeros_like, ones_like, empty_like, full_like, ensuring that we can override the
shape, chunks, dtype and fill_value of the array-like object provided to these functions with
appropriate keyword arguments
"""
ref_fill = 100
ref_arr = zarr.create_array(
store={},
shape=(11, 12),
dtype="uint8",
chunks=(11, 12),
zarr_format=zarr_format,
fill_value=ref_fill,
)
kwargs: dict[str, object] = {}
if func is zarr.api.asynchronous.full_like:
if out_fill == "keep":
expect_fill = ref_fill
else:
expect_fill = out_fill
kwargs["fill_value"] = expect_fill
elif func is zarr.api.asynchronous.zeros_like:
expect_fill = 0
elif func is zarr.api.asynchronous.ones_like:
expect_fill = 1
elif func is zarr.api.asynchronous.empty_like:
if out_fill == "keep":
expect_fill = ref_fill
else:
kwargs["fill_value"] = out_fill
expect_fill = out_fill
elif func is zarr.api.asynchronous.open_like: # type: ignore[comparison-overlap]
if out_fill == "keep":
expect_fill = ref_fill
else:
kwargs["fill_value"] = out_fill
expect_fill = out_fill
kwargs["mode"] = "w"
else:
raise AssertionError
if out_shape != "keep":
kwargs["shape"] = out_shape
expect_shape = out_shape
else:
expect_shape = ref_arr.shape
if out_chunks != "keep":
kwargs["chunks"] = out_chunks
expect_chunks = out_chunks
else:
expect_chunks = ref_arr.chunks
if out_dtype != "keep":
kwargs["dtype"] = out_dtype
expect_dtype = out_dtype
else:
expect_dtype = ref_arr.dtype # type: ignore[assignment]

new_arr = await func(ref_arr, path="foo", zarr_format=zarr_format, **kwargs) # type: ignore[call-arg]
assert new_arr.shape == expect_shape
assert new_arr.chunks == expect_chunks
assert new_arr.dtype == expect_dtype
assert np.all(Array(new_arr)[:] == expect_fill)


# TODO: parametrize over everything this function takes
@pytest.mark.parametrize("store", ["memory"], indirect=True)
def test_create_array(store: Store, zarr_format: ZarrFormat) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_api/test_asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def test_like_args(
"""
Test the like_args function
"""
assert _like_args(observed, {}) == expected
assert _like_args(observed) == expected


async def test_open_no_array() -> None:
Expand Down
62 changes: 61 additions & 1 deletion tests/test_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import re
import time
import warnings
from typing import TYPE_CHECKING, Any, Literal
from typing import TYPE_CHECKING, Any, Literal, get_args

import numpy as np
import pytest
Expand Down Expand Up @@ -761,6 +761,66 @@ def test_group_create_array(
assert np.array_equal(array[:], data)


LikeMethodName = Literal["zeros_like", "ones_like", "empty_like", "full_like"]


@pytest.mark.parametrize("method_name", get_args(LikeMethodName))
@pytest.mark.parametrize("out_shape", ["keep", (10, 10)])
@pytest.mark.parametrize("out_chunks", ["keep", (10, 10)])
@pytest.mark.parametrize("out_dtype", ["keep", "int8"])
def test_group_array_like_creation(
zarr_format: ZarrFormat,
method_name: LikeMethodName,
out_shape: Literal["keep"] | tuple[int, ...],
out_chunks: Literal["keep"] | tuple[int, ...],
out_dtype: str,
) -> None:
"""
Test Group.{zeros_like, ones_like, empty_like, full_like}, ensuring that we can override the
shape, chunks, and dtype of the array-like object provided to these functions with
appropriate keyword arguments
"""
ref_arr = zarr.ones(store={}, shape=(11, 12), dtype="uint8", chunks=(11, 12))
group = Group.from_store({}, zarr_format=zarr_format)
kwargs = {}
if method_name == "full_like":
expect_fill = 4
kwargs["fill_value"] = expect_fill
meth = group.full_like
elif method_name == "zeros_like":
expect_fill = 0
meth = group.zeros_like
elif method_name == "ones_like":
expect_fill = 1
meth = group.ones_like
elif method_name == "empty_like":
expect_fill = ref_arr.fill_value
meth = group.empty_like
else:
raise AssertionError
if out_shape != "keep":
kwargs["shape"] = out_shape
expect_shape = out_shape
else:
expect_shape = ref_arr.shape
if out_chunks != "keep":
kwargs["chunks"] = out_chunks
expect_chunks = out_chunks
else:
expect_chunks = ref_arr.chunks
if out_dtype != "keep":
kwargs["dtype"] = out_dtype
expect_dtype = out_dtype
else:
expect_dtype = ref_arr.dtype

new_arr = meth(name="foo", data=ref_arr, **kwargs)
assert new_arr.shape == expect_shape
assert new_arr.chunks == expect_chunks
assert new_arr.dtype == expect_dtype
assert np.all(new_arr[:] == expect_fill)


def test_group_array_creation(
store: Store,
zarr_format: ZarrFormat,
Expand Down
Loading