Skip to content

Commit 7729d92

Browse files
authored
fix: keep fill value and attributes in from_array (#4288)
`from_array` discards both `fill_value` and `attributes`. An explicit `fill_value=None` now means the dtype's default scalar (v3) or a null fill value (v2), consistent with `create_array`, and an empty `attributes` dict opts out of copying the source's attributes. Fixes #4287
1 parent d44f9f9 commit 7729d92

5 files changed

Lines changed: 88 additions & 24 deletions

File tree

changes/4288.bugfix.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
`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.
2+
3+
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.

src/zarr/api/synchronous.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,8 @@ def from_array(
11051105
fill_value : Any, optional
11061106
Fill value for the array.
11071107
If not specified, defaults to the fill value of the data array.
1108+
Pass `None` explicitly to use the default scalar of the data type
1109+
(Zarr format 3) or a null fill value (Zarr format 2) instead.
11081110
order : {"C", "F"}, optional
11091111
The memory order of the array (default is "C").
11101112
For Zarr format 2, this parameter sets the memory order of the array.
@@ -1118,6 +1120,7 @@ def from_array(
11181120
attributes : dict, optional
11191121
Attributes for the array.
11201122
If not specified, defaults to the attributes of the data array.
1123+
Pass an empty dict to create the array with no attributes.
11211124
chunk_key_encoding : ChunkKeyEncoding, optional
11221125
A specification of how the chunk keys are represented in storage.
11231126
For Zarr format 3, the default is `{"name": "default", "separator": "/"}}`.

src/zarr/core/array.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4179,6 +4179,8 @@ async def from_array(
41794179
fill_value : Any, optional
41804180
Fill value for the array.
41814181
If not specified, defaults to the fill value of the data array.
4182+
Pass `None` explicitly to use the default scalar of the data type
4183+
(Zarr format 3) or a null fill value (Zarr format 2) instead.
41824184
order : {"C", "F"}, optional
41834185
The memory order of the array (default is "C").
41844186
For Zarr format 2, this parameter sets the memory order of the array.
@@ -4192,6 +4194,7 @@ async def from_array(
41924194
attributes : dict, optional
41934195
Attributes for the array.
41944196
If not specified, defaults to the attributes of the data array.
4197+
Pass an empty dict to create the array with no attributes.
41954198
chunk_key_encoding : ChunkKeyEncoding, optional
41964199
A specification of how the chunk keys are represented in storage.
41974200
For Zarr format 3, the default is `{"name": "default", "separator": "/"}}`.
@@ -4276,6 +4279,7 @@ async def from_array(
42764279
zarr_format,
42774280
chunk_key_encoding,
42784281
dimension_names,
4282+
attributes,
42794283
) = _parse_keep_array_attr(
42804284
data=data,
42814285
chunks=chunks,
@@ -4288,6 +4292,7 @@ async def from_array(
42884292
zarr_format=zarr_format,
42894293
chunk_key_encoding=chunk_key_encoding,
42904294
dimension_names=dimension_names,
4295+
attributes=attributes,
42914296
)
42924297
if not hasattr(data, "dtype") or not hasattr(data, "shape"):
42934298
data = np.array(data)
@@ -4772,6 +4777,7 @@ def _parse_keep_array_attr(
47724777
zarr_format: ZarrFormat | None,
47734778
chunk_key_encoding: ChunkKeyEncodingLike | None,
47744779
dimension_names: DimensionNamesLike,
4780+
attributes: dict[str, JSON] | None,
47754781
) -> tuple[
47764782
ChunksLike | Literal["auto"],
47774783
ShardsLike | None,
@@ -4783,6 +4789,7 @@ def _parse_keep_array_attr(
47834789
ZarrFormat,
47844790
ChunkKeyEncodingLike | None,
47854791
DimensionNamesLike,
4792+
dict[str, JSON] | None,
47864793
]:
47874794
if isinstance(data, Array):
47884795
if chunks == "keep":
@@ -4809,7 +4816,7 @@ def _parse_keep_array_attr(
48094816
serializer = cast("SerializerLike", data.serializer)
48104817
else:
48114818
serializer = "auto"
4812-
if fill_value is None:
4819+
if fill_value is DEFAULT_FILL_VALUE:
48134820
fill_value = data.fill_value
48144821

48154822
if data.metadata.zarr_format == 2 and zarr_format == 3 and data.order == "F":
@@ -4830,6 +4837,8 @@ def _parse_keep_array_attr(
48304837
chunk_key_encoding = data.metadata.chunk_key_encoding
48314838
if dimension_names is None and data.metadata.zarr_format == 3:
48324839
dimension_names = data.metadata.dimension_names
4840+
if attributes is None:
4841+
attributes = dict(data.attrs)
48334842
else:
48344843
if chunks == "keep":
48354844
chunks = "auto"
@@ -4856,6 +4865,7 @@ def _parse_keep_array_attr(
48564865
zarr_format,
48574866
chunk_key_encoding,
48584867
dimension_names,
4868+
attributes,
48594869
)
48604870

48614871

src/zarr/testing/stateful.py

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -152,29 +152,14 @@ def add_array(self, data: DataObject, name: str) -> None:
152152
)
153153
note(f"Adding array: path='{path}' shape={a.shape} chunks={a.metadata.chunk_grid}")
154154

155-
# Recreate the same array in the store under test
156-
from zarr.core.metadata.v3 import RectilinearChunkGridMetadata, RegularChunkGridMetadata
157-
158-
chunk_grid = a.metadata.chunk_grid
159-
chunks_param: tuple[int, ...] | list[list[int]]
160-
if isinstance(chunk_grid, RectilinearChunkGridMetadata):
161-
chunks_param = [
162-
list(dim) if isinstance(dim, tuple) else [dim] for dim in chunk_grid.chunk_shapes
163-
]
164-
elif isinstance(chunk_grid, RegularChunkGridMetadata):
165-
chunks_param = chunk_grid.chunk_shape
166-
else:
167-
chunks_param = a.chunks
168-
169-
root = zarr.open_group(store=self.store, mode="a")
170-
arr = root.create_array(
171-
path,
172-
shape=a.shape,
173-
chunks=chunks_param,
174-
dtype=a.dtype,
175-
fill_value=a.fill_value,
176-
dimension_names=a.metadata.dimension_names, # type: ignore[union-attr]
177-
compressors=None,
155+
# Recreate the same array in the store under test.
156+
# The data is copied here rather than by `write_data=True`,
157+
# whose shard-wise copy does not support rectilinear chunk grids.
158+
arr = zarr.from_array(
159+
self.store,
160+
data=a,
161+
name=path,
162+
write_data=False,
178163
)
179164
arr[:] = a[:]
180165
self.all_arrays.add(path)

tests/test_array.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1826,6 +1826,69 @@ async def test_from_array_arraylike(
18261826
np.testing.assert_array_equal(result[...], np.full_like(src, fill_value))
18271827

18281828

1829+
@pytest.mark.parametrize("store", ["local", "memory"], indirect=True)
1830+
def test_from_array_keeps_fill_value_and_attributes(store: Store, zarr_format: ZarrFormat) -> None:
1831+
"""`from_array` defaults to the fill value and attributes of the source array."""
1832+
attributes: dict[str, JSON] = {"units": "K"}
1833+
src = zarr.create_array(
1834+
store,
1835+
name="src",
1836+
shape=(4,),
1837+
dtype="int32",
1838+
fill_value=42,
1839+
attributes=attributes,
1840+
zarr_format=zarr_format,
1841+
)
1842+
src[:] = np.arange(4, dtype="int32")
1843+
1844+
result = zarr.from_array({}, data=src)
1845+
assert result.fill_value == 42
1846+
assert dict(result.attrs) == attributes
1847+
1848+
# A metadata-only copy must read back the source's fill value, not the dtype default.
1849+
meta_only = zarr.from_array({}, data=src, write_data=False)
1850+
np.testing.assert_array_equal(meta_only[:], np.full((4,), 42, dtype="int32"))
1851+
1852+
1853+
@pytest.mark.parametrize("store", ["memory"], indirect=True)
1854+
def test_from_array_explicit_fill_value_and_attributes_override(
1855+
store: Store, zarr_format: ZarrFormat
1856+
) -> None:
1857+
"""Explicit `fill_value` / `attributes` arguments take precedence over the source.
1858+
1859+
An explicit ``fill_value=None`` selects the dtype's default scalar for Zarr format 3
1860+
and a null fill value for Zarr format 2, matching `create_array`, rather than being
1861+
treated as "keep the source's fill value". An empty ``attributes`` dict is likewise
1862+
honoured, so it is possible to drop the source's attributes.
1863+
"""
1864+
src = zarr.create_array(
1865+
store,
1866+
name="src",
1867+
shape=(4,),
1868+
dtype="int32",
1869+
fill_value=42,
1870+
attributes={"units": "K"},
1871+
zarr_format=zarr_format,
1872+
)
1873+
1874+
assert zarr.from_array({}, data=src, fill_value=7).fill_value == 7
1875+
assert dict(zarr.from_array({}, data=src, attributes={}).attrs) == {}
1876+
assert dict(zarr.from_array({}, data=src, attributes={"a": 1}).attrs) == {"a": 1}
1877+
1878+
explicit_none = zarr.from_array({}, data=src, fill_value=None)
1879+
if zarr_format == 2:
1880+
assert explicit_none.fill_value is None
1881+
else:
1882+
assert explicit_none.fill_value == 0 # int32 default scalar
1883+
1884+
1885+
def test_from_array_arraylike_gains_no_attributes() -> None:
1886+
"""A non-Array source has no attributes or fill value to keep."""
1887+
result = zarr.from_array({}, data=np.arange(4, dtype="int32"))
1888+
assert dict(result.attrs) == {}
1889+
assert result.fill_value == 0
1890+
1891+
18291892
def test_from_array_F_order() -> None:
18301893
arr = zarr.create_array(store={}, data=np.array([1]), order="F", zarr_format=2)
18311894
with pytest.warns(

0 commit comments

Comments
 (0)