Skip to content

Commit 6ff67e5

Browse files
committed
Merge branch 'main' into zipstore-from-path
2 parents 70f1344 + ee60792 commit 6ff67e5

File tree

7 files changed

+83
-38
lines changed

7 files changed

+83
-38
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/2850.fix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a bug where ``StorePath`` creation would not apply standard path normalization to the ``path`` parameter,
2+
which led to the creation of arrays and groups with invalid keys.

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/storage/_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class StorePath:
4242

4343
def __init__(self, store: Store, path: str = "") -> None:
4444
self.store = store
45-
self.path = path
45+
self.path = normalize_path(path)
4646

4747
@property
4848
def read_only(self) -> bool:

src/zarr/testing/strategies.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from zarr.core.sync import sync
2020
from zarr.storage import MemoryStore, StoreLike
2121
from zarr.storage._common import _dereference_path
22+
from zarr.storage._utils import normalize_path
2223

2324
# Copied from Xarray
2425
_attr_keys = st.text(st.characters(), min_size=1)
@@ -277,11 +278,12 @@ def arrays(
277278
if a.metadata.zarr_format == 3:
278279
assert a.fill_value is not None
279280
assert a.name is not None
281+
assert a.path == normalize_path(array_path)
282+
assert a.name == "/" + a.path
280283
assert isinstance(root[array_path], Array)
281284
assert nparray.shape == a.shape
282285
assert chunk_shape == a.chunks
283286
assert shard_shape == a.shards
284-
assert array_path == a.path, (path, name, array_path, a.name, a.path)
285287
assert a.basename == name, (a.basename, name)
286288
assert dict(a.attrs) == expected_attrs
287289

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

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -130,24 +130,27 @@ async def test_create_creates_parents(store: Store, zarr_format: ZarrFormat) ->
130130
assert g.attrs == {}
131131

132132

133-
def test_group_name_properties(store: Store, zarr_format: ZarrFormat) -> None:
133+
@pytest.mark.parametrize("store", ["memory"], indirect=True)
134+
@pytest.mark.parametrize("root_name", ["", "/", "a", "/a"])
135+
@pytest.mark.parametrize("branch_name", ["foo", "/foo", "foo/bar", "/foo/bar"])
136+
def test_group_name_properties(
137+
store: Store, zarr_format: ZarrFormat, root_name: str, branch_name: str
138+
) -> None:
134139
"""
135-
Test basic properties of groups
140+
Test that the path, name, and basename attributes of a group and its subgroups are consistent
136141
"""
137-
root = Group.from_store(store=store, zarr_format=zarr_format)
138-
assert root.path == ""
139-
assert root.name == "/"
140-
assert root.basename == ""
142+
root = Group.from_store(store=StorePath(store=store, path=root_name), zarr_format=zarr_format)
143+
assert root.path == normalize_path(root_name)
144+
assert root.name == "/" + root.path
145+
assert root.basename == root.path
141146

142-
foo = root.create_group("foo")
143-
assert foo.path == "foo"
144-
assert foo.name == "/foo"
145-
assert foo.basename == "foo"
146-
147-
bar = root.create_group("foo/bar")
148-
assert bar.path == "foo/bar"
149-
assert bar.name == "/foo/bar"
150-
assert bar.basename == "bar"
147+
branch = root.create_group(branch_name)
148+
if root.path == "":
149+
assert branch.path == normalize_path(branch_name)
150+
else:
151+
assert branch.path == "/".join([root.path, normalize_path(branch_name)])
152+
assert branch.name == "/" + branch.path
153+
assert branch.basename == branch_name.split("/")[-1]
151154

152155

153156
@pytest.mark.parametrize("consolidated_metadata", [True, False])
@@ -623,11 +626,13 @@ async def test_group_update_attributes_async(store: Store, zarr_format: ZarrForm
623626

624627

625628
@pytest.mark.parametrize("method", ["create_array", "array"])
629+
@pytest.mark.parametrize("name", ["a", "/a"])
626630
def test_group_create_array(
627631
store: Store,
628632
zarr_format: ZarrFormat,
629633
overwrite: bool,
630634
method: Literal["create_array", "array"],
635+
name: str,
631636
) -> None:
632637
"""
633638
Test `Group.from_store`
@@ -638,23 +643,26 @@ def test_group_create_array(
638643
data = np.arange(np.prod(shape)).reshape(shape).astype(dtype)
639644

640645
if method == "create_array":
641-
array = group.create_array(name="array", shape=shape, dtype=dtype)
646+
array = group.create_array(name=name, shape=shape, dtype=dtype)
642647
array[:] = data
643648
elif method == "array":
644649
with pytest.warns(DeprecationWarning):
645-
array = group.array(name="array", data=data, shape=shape, dtype=dtype)
650+
array = group.array(name=name, data=data, shape=shape, dtype=dtype)
646651
else:
647652
raise AssertionError
648653

649654
if not overwrite:
650655
if method == "create_array":
651656
with pytest.raises(ContainsArrayError):
652-
a = group.create_array(name="array", shape=shape, dtype=dtype)
657+
a = group.create_array(name=name, shape=shape, dtype=dtype)
653658
a[:] = data
654659
elif method == "array":
655660
with pytest.raises(ContainsArrayError), pytest.warns(DeprecationWarning):
656-
a = group.array(name="array", shape=shape, dtype=dtype)
661+
a = group.array(name=name, shape=shape, dtype=dtype)
657662
a[:] = data
663+
664+
assert array.path == normalize_path(name)
665+
assert array.name == "/" + array.path
658666
assert array.shape == shape
659667
assert array.dtype == np.dtype(dtype)
660668
assert np.array_equal(array[:], data)
@@ -945,20 +953,23 @@ async def test_asyncgroup_delitem(store: Store, zarr_format: ZarrFormat) -> None
945953
raise AssertionError
946954

947955

956+
@pytest.mark.parametrize("name", ["a", "/a"])
948957
async def test_asyncgroup_create_group(
949958
store: Store,
959+
name: str,
950960
zarr_format: ZarrFormat,
951961
) -> None:
952962
agroup = await AsyncGroup.from_store(store=store, zarr_format=zarr_format)
953-
sub_node_path = "sub_group"
954963
attributes = {"foo": 999}
955-
subnode = await agroup.create_group(name=sub_node_path, attributes=attributes)
956-
957-
assert isinstance(subnode, AsyncGroup)
958-
assert subnode.attrs == attributes
959-
assert subnode.store_path.path == sub_node_path
960-
assert subnode.store_path.store == store
961-
assert subnode.metadata.zarr_format == zarr_format
964+
subgroup = await agroup.create_group(name=name, attributes=attributes)
965+
966+
assert isinstance(subgroup, AsyncGroup)
967+
assert subgroup.path == normalize_path(name)
968+
assert subgroup.name == "/" + subgroup.path
969+
assert subgroup.attrs == attributes
970+
assert subgroup.store_path.path == subgroup.path
971+
assert subgroup.store_path.store == store
972+
assert subgroup.metadata.zarr_format == zarr_format
962973

963974

964975
async def test_asyncgroup_create_array(

0 commit comments

Comments
 (0)