Skip to content
Merged
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
3 changes: 3 additions & 0 deletions changes/4288.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
`zarr.from_array` now defaults to the fill value and the attributes of the source array. Previously both were silently discarded: the array was created with the data type's default scalar and no attributes.

An explicit `fill_value=None` now selects the data type's default scalar (Zarr format 3) or a null fill value (Zarr format 2), consistently with `create_array`, and an empty `attributes` dict creates the array with no attributes.
3 changes: 3 additions & 0 deletions src/zarr/api/synchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,8 @@ def from_array(
fill_value : Any, optional
Fill value for the array.
If not specified, defaults to the fill value of the data array.
Pass `None` explicitly to use the default scalar of the data type
(Zarr format 3) or a null fill value (Zarr format 2) instead.
order : {"C", "F"}, optional
The memory order of the array (default is "C").
For Zarr format 2, this parameter sets the memory order of the array.
Expand All @@ -1118,6 +1120,7 @@ def from_array(
attributes : dict, optional
Attributes for the array.
If not specified, defaults to the attributes of the data array.
Pass an empty dict to create the array with no attributes.
chunk_key_encoding : ChunkKeyEncoding, optional
A specification of how the chunk keys are represented in storage.
For Zarr format 3, the default is `{"name": "default", "separator": "/"}}`.
Expand Down
12 changes: 11 additions & 1 deletion src/zarr/core/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -4179,6 +4179,8 @@ async def from_array(
fill_value : Any, optional
Fill value for the array.
If not specified, defaults to the fill value of the data array.
Pass `None` explicitly to use the default scalar of the data type
(Zarr format 3) or a null fill value (Zarr format 2) instead.
order : {"C", "F"}, optional
The memory order of the array (default is "C").
For Zarr format 2, this parameter sets the memory order of the array.
Expand All @@ -4192,6 +4194,7 @@ async def from_array(
attributes : dict, optional
Attributes for the array.
If not specified, defaults to the attributes of the data array.
Pass an empty dict to create the array with no attributes.
chunk_key_encoding : ChunkKeyEncoding, optional
A specification of how the chunk keys are represented in storage.
For Zarr format 3, the default is `{"name": "default", "separator": "/"}}`.
Expand Down Expand Up @@ -4276,6 +4279,7 @@ async def from_array(
zarr_format,
chunk_key_encoding,
dimension_names,
attributes,
) = _parse_keep_array_attr(
data=data,
chunks=chunks,
Expand All @@ -4288,6 +4292,7 @@ async def from_array(
zarr_format=zarr_format,
chunk_key_encoding=chunk_key_encoding,
dimension_names=dimension_names,
attributes=attributes,
)
if not hasattr(data, "dtype") or not hasattr(data, "shape"):
data = np.array(data)
Expand Down Expand Up @@ -4772,6 +4777,7 @@ def _parse_keep_array_attr(
zarr_format: ZarrFormat | None,
chunk_key_encoding: ChunkKeyEncodingLike | None,
dimension_names: DimensionNamesLike,
attributes: dict[str, JSON] | None,
) -> tuple[
ChunksLike | Literal["auto"],
ShardsLike | None,
Expand All @@ -4783,6 +4789,7 @@ def _parse_keep_array_attr(
ZarrFormat,
ChunkKeyEncodingLike | None,
DimensionNamesLike,
dict[str, JSON] | None,
]:
if isinstance(data, Array):
if chunks == "keep":
Expand All @@ -4809,7 +4816,7 @@ def _parse_keep_array_attr(
serializer = cast("SerializerLike", data.serializer)
else:
serializer = "auto"
if fill_value is None:
if fill_value is DEFAULT_FILL_VALUE:
fill_value = data.fill_value

if data.metadata.zarr_format == 2 and zarr_format == 3 and data.order == "F":
Expand All @@ -4830,6 +4837,8 @@ def _parse_keep_array_attr(
chunk_key_encoding = data.metadata.chunk_key_encoding
if dimension_names is None and data.metadata.zarr_format == 3:
dimension_names = data.metadata.dimension_names
if attributes is None:
attributes = dict(data.attrs)
else:
if chunks == "keep":
chunks = "auto"
Expand All @@ -4856,6 +4865,7 @@ def _parse_keep_array_attr(
zarr_format,
chunk_key_encoding,
dimension_names,
attributes,
)


Expand Down
31 changes: 8 additions & 23 deletions src/zarr/testing/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,29 +152,14 @@ def add_array(self, data: DataObject, name: str) -> None:
)
note(f"Adding array: path='{path}' shape={a.shape} chunks={a.metadata.chunk_grid}")

# Recreate the same array in the store under test
from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata

chunk_grid = a.metadata.chunk_grid
chunks_param: tuple[int, ...] | list[list[int]]
if isinstance(chunk_grid, RectilinearChunkGridMetadata):
chunks_param = [
list(dim) if isinstance(dim, tuple) else [dim] for dim in chunk_grid.chunk_shapes
]
elif isinstance(chunk_grid, RegularChunkGridMetadata):
chunks_param = chunk_grid.chunk_shape
else:
chunks_param = a.chunks

root = zarr.open_group(store=self.store, mode="a")
arr = root.create_array(
path,
shape=a.shape,
chunks=chunks_param,
dtype=a.dtype,
fill_value=a.fill_value,
dimension_names=a.metadata.dimension_names, # type: ignore[union-attr]
compressors=None,
# Recreate the same array in the store under test.
# The data is copied here rather than by `write_data=True`,
# whose shard-wise copy does not support rectilinear chunk grids.
arr = zarr.from_array(
self.store,
data=a,
name=path,
write_data=False,
)
arr[:] = a[:]
self.all_arrays.add(path)
Expand Down
63 changes: 63 additions & 0 deletions tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,69 @@ async def test_from_array_arraylike(
np.testing.assert_array_equal(result[...], np.full_like(src, fill_value))


@pytest.mark.parametrize("store", ["local", "memory"], indirect=True)
def test_from_array_keeps_fill_value_and_attributes(store: Store, zarr_format: ZarrFormat) -> None:
"""`from_array` defaults to the fill value and attributes of the source array."""
attributes: dict[str, JSON] = {"units": "K"}
src = zarr.create_array(
store,
name="src",
shape=(4,),
dtype="int32",
fill_value=42,
attributes=attributes,
zarr_format=zarr_format,
)
src[:] = np.arange(4, dtype="int32")

result = zarr.from_array({}, data=src)
assert result.fill_value == 42
assert dict(result.attrs) == attributes

# A metadata-only copy must read back the source's fill value, not the dtype default.
meta_only = zarr.from_array({}, data=src, write_data=False)
np.testing.assert_array_equal(meta_only[:], np.full((4,), 42, dtype="int32"))


@pytest.mark.parametrize("store", ["memory"], indirect=True)
def test_from_array_explicit_fill_value_and_attributes_override(
store: Store, zarr_format: ZarrFormat
) -> None:
"""Explicit `fill_value` / `attributes` arguments take precedence over the source.

An explicit ``fill_value=None`` selects the dtype's default scalar for Zarr format 3
and a null fill value for Zarr format 2, matching `create_array`, rather than being
treated as "keep the source's fill value". An empty ``attributes`` dict is likewise
honoured, so it is possible to drop the source's attributes.
"""
src = zarr.create_array(
store,
name="src",
shape=(4,),
dtype="int32",
fill_value=42,
attributes={"units": "K"},
zarr_format=zarr_format,
)

assert zarr.from_array({}, data=src, fill_value=7).fill_value == 7
assert dict(zarr.from_array({}, data=src, attributes={}).attrs) == {}
assert dict(zarr.from_array({}, data=src, attributes={"a": 1}).attrs) == {"a": 1}

explicit_none = zarr.from_array({}, data=src, fill_value=None)
if zarr_format == 2:
assert explicit_none.fill_value is None
else:
assert explicit_none.fill_value == 0 # int32 default scalar


def test_from_array_arraylike_gains_no_attributes() -> None:
"""A non-Array source has no attributes or fill value to keep."""
result = zarr.from_array({}, data=np.arange(4, dtype="int32"))
assert dict(result.attrs) == {}
assert result.fill_value == 0


def test_from_array_F_order() -> None:
arr = zarr.create_array(store={}, data=np.array([1]), order="F", zarr_format=2)
with pytest.warns(
Expand Down
Loading