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
1 change: 1 addition & 0 deletions changes/4325.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`zarr.from_array` now deep-copies the source array's attributes instead of sharing nested dicts and lists between the source and the new array. Previously, mutating a nested attribute on the new array (for example ``dst.attrs["meta"]["tags"].append(...)``) silently changed the source array's in-memory attributes too.
5 changes: 4 additions & 1 deletion src/zarr/core/array.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import copy
import math
import warnings
from asyncio import gather
Expand Down Expand Up @@ -4955,7 +4956,9 @@ def _parse_keep_array_attr(
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)
# Deep copy so nested containers are not shared between the source
# array's in-memory metadata and the new array's.
attributes = copy.deepcopy(dict(data.attrs))
else:
if chunks == "keep":
chunks = "auto"
Expand Down
14 changes: 13 additions & 1 deletion tests/test_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -1829,7 +1829,7 @@ async def test_from_array_arraylike(
@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"}
attributes: dict[str, JSON] = {"units": "K", "nested": {"x": [1]}, "tags": ["a"]}
src = zarr.create_array(
store,
name="src",
Expand All @@ -1845,6 +1845,18 @@ def test_from_array_keeps_fill_value_and_attributes(store: Store, zarr_format: Z
assert result.fill_value == 42
assert dict(result.attrs) == attributes

# The copied attributes must not alias the source's nested containers.
nested = result.attrs["nested"]
assert isinstance(nested, dict)
nested_x = nested["x"]
assert isinstance(nested_x, list)
nested_x.append(99)
tags = result.attrs["tags"]
assert isinstance(tags, list)
tags.append("b")
assert src.attrs["nested"] == {"x": [1]}
assert src.attrs["tags"] == ["a"]

# 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"))
Expand Down
Loading