Skip to content

Commit 7520870

Browse files
authored
Prevent update_attributes from erasing all prior attributes (#2870)
* Prevent update_attributes from erasing all prior attributes * Fix incorrect group attribute update tests * Add changelog entry
1 parent 64b9a37 commit 7520870

File tree

7 files changed

+50
-8
lines changed

7 files changed

+50
-8
lines changed

changes/2870.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prevent update_attributes calls from deleting old attributes

src/zarr/core/array.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,8 +1609,6 @@ async def update_attributes(self, new_attributes: dict[str, JSON]) -> Self:
16091609
- The updated attributes will be merged with existing attributes, and any conflicts will be
16101610
overwritten by the new values.
16111611
"""
1612-
# metadata.attributes is "frozen" so we simply clear and update the dict
1613-
self.metadata.attributes.clear()
16141612
self.metadata.attributes.update(new_attributes)
16151613

16161614
# Write new metadata

src/zarr/core/attributes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def put(self, d: dict[str, JSON]) -> None:
5050
>>> attrs
5151
{'a': 3, 'c': 4}
5252
"""
53+
self._obj.metadata.attributes.clear()
5354
self._obj = self._obj.update_attributes(d)
5455

5556
def asdict(self) -> dict[str, JSON]:

src/zarr/core/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import functools
55
import operator
66
import warnings
7-
from collections.abc import Iterable, Mapping
7+
from collections.abc import Iterable, Mapping, Sequence
88
from enum import Enum
99
from itertools import starmap
1010
from typing import (
@@ -37,7 +37,7 @@
3737
ChunkCoordsLike = Iterable[int]
3838
ZarrFormat = Literal[2, 3]
3939
NodeType = Literal["array", "group"]
40-
JSON = str | int | float | Mapping[str, "JSON"] | tuple["JSON", ...] | None
40+
JSON = str | int | float | Mapping[str, "JSON"] | Sequence["JSON"] | None
4141
MemoryOrder = Literal["C", "F"]
4242
AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"]
4343

src/zarr/core/group.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1254,8 +1254,6 @@ async def update_attributes(self, new_attributes: dict[str, Any]) -> AsyncGroup:
12541254
-------
12551255
self : AsyncGroup
12561256
"""
1257-
# metadata.attributes is "frozen" so we simply clear and update the dict
1258-
self.metadata.attributes.clear()
12591257
self.metadata.attributes.update(new_attributes)
12601258

12611259
# Write new metadata

tests/test_attributes.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,42 @@ def test_asdict() -> None:
2020
)
2121
result = attrs.asdict()
2222
assert result == {"a": 1, "b": 2}
23+
24+
25+
def test_update_attributes_preserves_existing() -> None:
26+
"""
27+
Test that `update_attributes` only updates the specified attributes
28+
and preserves existing ones.
29+
"""
30+
store = zarr.storage.MemoryStore()
31+
z = zarr.create(10, store=store, overwrite=True)
32+
z.attrs["a"] = []
33+
z.attrs["b"] = 3
34+
assert dict(z.attrs) == {"a": [], "b": 3}
35+
36+
z.update_attributes({"a": [3, 4], "c": 4})
37+
assert dict(z.attrs) == {"a": [3, 4], "b": 3, "c": 4}
38+
39+
40+
def test_update_empty_attributes() -> None:
41+
"""
42+
Ensure updating when initial attributes are empty works.
43+
"""
44+
store = zarr.storage.MemoryStore()
45+
z = zarr.create(10, store=store, overwrite=True)
46+
assert dict(z.attrs) == {}
47+
z.update_attributes({"a": [3, 4], "c": 4})
48+
assert dict(z.attrs) == {"a": [3, 4], "c": 4}
49+
50+
51+
def test_update_no_changes() -> None:
52+
"""
53+
Ensure updating when no new or modified attributes does not alter existing ones.
54+
"""
55+
store = zarr.storage.MemoryStore()
56+
z = zarr.create(10, store=store, overwrite=True)
57+
z.attrs["a"] = []
58+
z.attrs["b"] = 3
59+
60+
z.update_attributes({})
61+
assert dict(z.attrs) == {"a": [], "b": 3}

tests/test_group.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,10 @@ def test_group_update_attributes(store: Store, zarr_format: ZarrFormat) -> None:
604604
assert group.attrs == attrs
605605
new_attrs = {"bar": 100}
606606
new_group = group.update_attributes(new_attrs)
607-
assert new_group.attrs == new_attrs
607+
608+
updated_attrs = attrs.copy()
609+
updated_attrs.update(new_attrs)
610+
assert new_group.attrs == updated_attrs
608611

609612

610613
async def test_group_update_attributes_async(store: Store, zarr_format: ZarrFormat) -> None:
@@ -1008,7 +1011,9 @@ async def test_asyncgroup_update_attributes(store: Store, zarr_format: ZarrForma
10081011
)
10091012

10101013
agroup_new_attributes = await agroup.update_attributes(attributes_new)
1011-
assert agroup_new_attributes.attrs == attributes_new
1014+
attributes_updated = attributes_old.copy()
1015+
attributes_updated.update(attributes_new)
1016+
assert agroup_new_attributes.attrs == attributes_updated
10121017

10131018

10141019
@pytest.mark.parametrize("store", ["local"], indirect=["store"])

0 commit comments

Comments
 (0)