Skip to content

Commit bf24d69

Browse files
committed
Merge branch 'main' of github.com:zarr-developers/zarr-python into feat/fixed-length-strings
2 parents d9b44b4 + 73d5a6c commit bf24d69

File tree

12 files changed

+94
-22
lines changed

12 files changed

+94
-22
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ci:
66
default_stages: [pre-commit, pre-push]
77
repos:
88
- repo: https://github.com/astral-sh/ruff-pre-commit
9-
rev: v0.9.4
9+
rev: v0.9.9
1010
hooks:
1111
- id: ruff
1212
args: ["--fix", "--show-fixes"]
@@ -22,7 +22,7 @@ repos:
2222
- id: check-yaml
2323
- id: trailing-whitespace
2424
- repo: https://github.com/pre-commit/mirrors-mypy
25-
rev: v1.14.1
25+
rev: v1.15.0
2626
hooks:
2727
- id: mypy
2828
files: src|tests

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

docs/user-guide/v3_migration.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,30 @@ The Store class
124124
The Store API has changed significant in Zarr-Python 3. The most notable changes to the
125125
Store API are:
126126

127+
Store Import Paths
128+
^^^^^^^^^^^^^^^^^^
129+
Several store implementations have moved from the top-level module to ``zarr.storage``:
130+
131+
.. code-block:: diff
132+
:caption: Store import changes from v2 to v3
133+
134+
# Before (v2)
135+
- from zarr import MemoryStore, DirectoryStore
136+
+ from zarr.storage import MemoryStore, LocalStore # LocalStore replaces DirectoryStore
137+
138+
Common replacements:
139+
140+
+-------------------------+------------------------------------+
141+
| v2 Import | v3 Import |
142+
+=========================+====================================+
143+
| ``zarr.MemoryStore`` | ``zarr.storage.MemoryStore`` |
144+
+-------------------------+------------------------------------+
145+
| ``zarr.DirectoryStore`` | ``zarr.storage.LocalStore`` |
146+
+-------------------------+------------------------------------+
147+
| ``zarr.TempStore`` | Use ``tempfile.TemporaryDirectory``|
148+
| | with ``LocalStore`` |
149+
+-------------------------+------------------------------------+
150+
127151
1. Replaced the ``MutableMapping`` base class in favor of a custom abstract base class
128152
(:class:`zarr.abc.store.Store`).
129153
2. Switched to an asynchronous interface for all store methods that result in IO. This

src/zarr/codecs/blosc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class BloscCname(Enum):
5555
zlib = "zlib"
5656

5757

58-
# See https://zarr.readthedocs.io/en/stable/tutorial.html#configuring-blosc
58+
# See https://zarr.readthedocs.io/en/stable/user-guide/performance.html#configuring-blosc
5959
numcodecs.blosc.use_threads = False
6060

6161

src/zarr/core/array.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,8 +1646,6 @@ async def update_attributes(self, new_attributes: dict[str, JSON]) -> Self:
16461646
- The updated attributes will be merged with existing attributes, and any conflicts will be
16471647
overwritten by the new values.
16481648
"""
1649-
# metadata.attributes is "frozen" so we simply clear and update the dict
1650-
self.metadata.attributes.clear()
16511649
self.metadata.attributes.update(new_attributes)
16521650

16531651
# 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 (
@@ -36,7 +36,7 @@
3636
ChunkCoordsLike = Iterable[int]
3737
ZarrFormat = Literal[2, 3]
3838
NodeType = Literal["array", "group"]
39-
JSON = str | int | float | Mapping[str, "JSON"] | tuple["JSON", ...] | None
39+
JSON = str | int | float | Mapping[str, "JSON"] | Sequence["JSON"] | None
4040
MemoryOrder = Literal["C", "F"]
4141
AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"]
4242

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_api.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ def test_save(store: Store, n_args: int, n_kwargs: int) -> None:
209209
assert isinstance(array, Array)
210210
assert_array_equal(array[:], data)
211211
else:
212-
save(store, *args, **kwargs) # type: ignore[arg-type]
212+
save(store, *args, **kwargs) # type: ignore [arg-type]
213213
group = open(store)
214214
assert isinstance(group, Group)
215215
for array in group.array_values():
@@ -1095,25 +1095,31 @@ async def test_open_falls_back_to_open_group_async() -> None:
10951095
assert group.attrs == {"key": "value"}
10961096

10971097

1098-
def test_open_mode_write_creates_group(tmp_path: pathlib.Path) -> None:
1098+
@pytest.mark.parametrize("mode", ["r", "r+", "w", "a"])
1099+
def test_open_modes_creates_group(tmp_path: pathlib.Path, mode: str) -> None:
10991100
# https://github.com/zarr-developers/zarr-python/issues/2490
1100-
zarr_dir = tmp_path / "test.zarr"
1101-
group = zarr.open(zarr_dir, mode="w")
1102-
assert isinstance(group, Group)
1101+
zarr_dir = tmp_path / f"mode-{mode}-test.zarr"
1102+
if mode in ["r", "r+"]:
1103+
# Expect FileNotFoundError to be raised if 'r' or 'r+' mode
1104+
with pytest.raises(FileNotFoundError):
1105+
zarr.open(store=zarr_dir, mode=mode)
1106+
else:
1107+
group = zarr.open(store=zarr_dir, mode=mode)
1108+
assert isinstance(group, Group)
11031109

11041110

11051111
async def test_metadata_validation_error() -> None:
11061112
with pytest.raises(
11071113
MetadataValidationError,
11081114
match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.",
11091115
):
1110-
await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore[arg-type]
1116+
await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore [arg-type]
11111117

11121118
with pytest.raises(
11131119
MetadataValidationError,
11141120
match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.",
11151121
):
1116-
await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore[arg-type]
1122+
await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore [arg-type]
11171123

11181124

11191125
@pytest.mark.parametrize(

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}

0 commit comments

Comments
 (0)