Skip to content

Commit 817617a

Browse files
authored
Merge branch 'main' into string-arguments-for-codecs
2 parents a55abad + 7584b96 commit 817617a

File tree

14 files changed

+175
-58
lines changed

14 files changed

+175
-58
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ repos:
3838
# Tests
3939
- pytest
4040
- hypothesis
41+
- s3fs
4142
- repo: https://github.com/scientific-python/cookie
4243
rev: 2025.01.22
4344
hooks:

changes/2862.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug that prevented the number of initialized chunks being counted properly.

docs/user-guide/groups.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ property. E.g.::
140140
No. bytes : 8000000 (7.6M)
141141
No. bytes stored : 1614
142142
Storage ratio : 4956.6
143-
Chunks Initialized : 0
143+
Chunks Initialized : 10
144144
>>> baz.info
145145
Type : Array
146146
Zarr format : 3

pyproject.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,11 @@ module = [
358358
"tests.package_with_entrypoint.*",
359359
"zarr.testing.stateful",
360360
"tests.test_codecs.test_transpose",
361-
"tests.test_config"
361+
"tests.test_config",
362+
"tests.test_store.test_zip",
363+
"tests.test_store.test_local",
364+
"tests.test_store.test_fsspec",
365+
"tests.test_store.test_memory",
362366
]
363367
strict = false
364368

@@ -368,7 +372,11 @@ strict = false
368372
module = [
369373
"tests.test_codecs.test_codecs",
370374
"tests.test_metadata.*",
371-
"tests.test_store.*",
375+
"tests.test_store.test_core",
376+
"tests.test_store.test_logging",
377+
"tests.test_store.test_object",
378+
"tests.test_store.test_stateful",
379+
"tests.test_store.test_wrapper",
372380
"tests.test_group",
373381
"tests.test_indexing",
374382
"tests.test_properties",

src/zarr/core/array.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
get_pipeline_class,
118118
)
119119
from zarr.storage._common import StorePath, ensure_no_existing_node, make_store_path
120+
from zarr.storage._utils import _relativize_path
120121

121122
if TYPE_CHECKING:
122123
from collections.abc import Iterator, Sequence
@@ -3737,7 +3738,12 @@ async def chunks_initialized(
37373738
store_contents = [
37383739
x async for x in array.store_path.store.list_prefix(prefix=array.store_path.path)
37393740
]
3740-
return tuple(chunk_key for chunk_key in array._iter_chunk_keys() if chunk_key in store_contents)
3741+
store_contents_relative = [
3742+
_relativize_path(path=key, prefix=array.store_path.path) for key in store_contents
3743+
]
3744+
return tuple(
3745+
chunk_key for chunk_key in array._iter_chunk_keys() if chunk_key in store_contents_relative
3746+
)
37413747

37423748

37433749
def _build_parents(

src/zarr/storage/_utils.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,62 @@ def _join_paths(paths: Iterable[str]) -> str:
7474
"""
7575
Filter out instances of '' and join the remaining strings with '/'.
7676
77-
Because the root node of a zarr hierarchy is represented by an empty string,
77+
Parameters
78+
----------
79+
paths : Iterable[str]
80+
81+
Returns
82+
-------
83+
str
84+
85+
Examples
86+
--------
87+
>>> _join_paths(["", "a", "b"])
88+
'a/b'
89+
>>> _join_paths(["a", "b", "c"])
90+
'a/b/c'
7891
"""
7992
return "/".join(filter(lambda v: v != "", paths))
8093

8194

95+
def _relativize_path(*, path: str, prefix: str) -> str:
96+
"""
97+
Make a "/"-delimited path relative to some prefix. If the prefix is '', then the path is
98+
returned as-is. Otherwise, the prefix is removed from the path as well as the separator
99+
string "/".
100+
101+
If ``prefix`` is not the empty string and ``path`` does not start with ``prefix``
102+
followed by a "/" character, then an error is raised.
103+
104+
This function assumes that the prefix does not end with "/".
105+
106+
Parameters
107+
----------
108+
path : str
109+
The path to make relative to the prefix.
110+
prefix : str
111+
The prefix to make the path relative to.
112+
113+
Returns
114+
-------
115+
str
116+
117+
Examples
118+
--------
119+
>>> _relativize_path(path="", prefix="a/b")
120+
'a/b'
121+
>>> _relativize_path(path="a/b", prefix="a/b/c")
122+
'c'
123+
"""
124+
if prefix == "":
125+
return path
126+
else:
127+
_prefix = prefix + "/"
128+
if not path.startswith(_prefix):
129+
raise ValueError(f"The first component of {path} does not start with {prefix}.")
130+
return path.removeprefix(f"{prefix}/")
131+
132+
82133
def _normalize_paths(paths: Iterable[str]) -> tuple[str, ...]:
83134
"""
84135
Normalize the input paths according to the normalization scheme used for zarr node paths.

src/zarr/testing/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def get(self, store: S, key: str) -> Buffer:
5858

5959
@abstractmethod
6060
@pytest.fixture
61-
def store_kwargs(self) -> dict[str, Any]:
61+
def store_kwargs(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
6262
"""Kwargs for instantiating a store"""
6363
...
6464

src/zarr/testing/utils.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

3-
from collections.abc import Callable, Coroutine
4-
from typing import TYPE_CHECKING, Any, TypeVar, cast
3+
from typing import TYPE_CHECKING, TypeVar, cast
54

65
import pytest
76

@@ -38,13 +37,13 @@ def has_cupy() -> bool:
3837
return False
3938

4039

41-
T_Callable = TypeVar("T_Callable", bound=Callable[..., Coroutine[Any, Any, None] | None])
40+
T = TypeVar("T")
4241

4342

4443
# Decorator for GPU tests
45-
def gpu_test(func: T_Callable) -> T_Callable:
44+
def gpu_test(func: T) -> T:
4645
return cast(
47-
"T_Callable",
46+
"T",
4847
pytest.mark.gpu(
4948
pytest.mark.skipif(not has_cupy(), reason="CuPy not installed or no GPU available")(
5049
func

tests/test_array.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -388,12 +388,13 @@ async def test_nchunks_initialized(test_cls: type[Array] | type[AsyncArray[Any]]
388388
assert observed == expected
389389

390390

391-
async def test_chunks_initialized() -> None:
391+
@pytest.mark.parametrize("path", ["", "foo"])
392+
async def test_chunks_initialized(path: str) -> None:
392393
"""
393394
Test that chunks_initialized accurately returns the keys of stored chunks.
394395
"""
395396
store = MemoryStore()
396-
arr = zarr.create_array(store, shape=(100,), chunks=(10,), dtype="i4")
397+
arr = zarr.create_array(store, name=path, shape=(100,), chunks=(10,), dtype="i4")
397398

398399
chunks_accumulated = tuple(
399400
accumulate(tuple(tuple(v.split(" ")) for v in arr._iter_chunk_keys()))

tests/test_store/test_core.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88
from zarr.core.common import AccessModeLiteral, ZarrFormat
99
from zarr.storage import FsspecStore, LocalStore, MemoryStore, StoreLike, StorePath
1010
from zarr.storage._common import contains_array, contains_group, make_store_path
11-
from zarr.storage._utils import _join_paths, _normalize_path_keys, _normalize_paths, normalize_path
11+
from zarr.storage._utils import (
12+
_join_paths,
13+
_normalize_path_keys,
14+
_normalize_paths,
15+
_relativize_path,
16+
normalize_path,
17+
)
1218

1319

1420
@pytest.mark.parametrize("path", ["foo", "foo/bar"])
@@ -221,3 +227,27 @@ def test_normalize_path_keys():
221227
"""
222228
data = {"a": 10, "//b": 10}
223229
assert _normalize_path_keys(data) == {normalize_path(k): v for k, v in data.items()}
230+
231+
232+
@pytest.mark.parametrize(
233+
("path", "prefix", "expected"),
234+
[
235+
("a", "", "a"),
236+
("a/b/c", "a/b", "c"),
237+
("a/b/c", "a", "b/c"),
238+
],
239+
)
240+
def test_relativize_path_valid(path: str, prefix: str, expected: str) -> None:
241+
"""
242+
Test the normal behavior of the _relativize_path function. Prefixes should be removed from the
243+
path argument.
244+
"""
245+
assert _relativize_path(path=path, prefix=prefix) == expected
246+
247+
248+
def test_relativize_path_invalid() -> None:
249+
path = "a/b/c"
250+
prefix = "b"
251+
msg = f"The first component of {path} does not start with {prefix}."
252+
with pytest.raises(ValueError, match=msg):
253+
_relativize_path(path="a/b/c", prefix="b")

tests/test_store/test_fsspec.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import os
55
import re
6-
from typing import TYPE_CHECKING
6+
from typing import TYPE_CHECKING, Any
77

88
import pytest
99
from packaging.version import parse as parse_version
@@ -17,8 +17,13 @@
1717

1818
if TYPE_CHECKING:
1919
from collections.abc import Generator
20+
from pathlib import Path
2021

2122
import botocore.client
23+
import s3fs
24+
25+
from zarr.core.common import JSON
26+
2227

2328
# Warning filter due to https://github.com/boto/boto3/issues/3889
2429
pytestmark = [
@@ -109,10 +114,13 @@ async def test_basic() -> None:
109114
data = b"hello"
110115
await store.set("foo", cpu.Buffer.from_bytes(data))
111116
assert await store.exists("foo")
112-
assert (await store.get("foo", prototype=default_buffer_prototype())).to_bytes() == data
117+
buf = await store.get("foo", prototype=default_buffer_prototype())
118+
assert buf is not None
119+
assert buf.to_bytes() == data
113120
out = await store.get_partial_values(
114121
prototype=default_buffer_prototype(), key_ranges=[("foo", OffsetByteRequest(1))]
115122
)
123+
assert out[0] is not None
116124
assert out[0].to_bytes() == data[1:]
117125

118126

@@ -121,7 +129,7 @@ class TestFsspecStoreS3(StoreTests[FsspecStore, cpu.Buffer]):
121129
buffer_cls = cpu.Buffer
122130

123131
@pytest.fixture
124-
def store_kwargs(self, request) -> dict[str, str | bool]:
132+
def store_kwargs(self) -> dict[str, str | bool]:
125133
try:
126134
from fsspec import url_to_fs
127135
except ImportError:
@@ -133,7 +141,7 @@ def store_kwargs(self, request) -> dict[str, str | bool]:
133141
return {"fs": fs, "path": path}
134142

135143
@pytest.fixture
136-
def store(self, store_kwargs: dict[str, str | bool]) -> FsspecStore:
144+
async def store(self, store_kwargs: dict[str, Any]) -> FsspecStore:
137145
return self.store_cls(**store_kwargs)
138146

139147
async def get(self, store: FsspecStore, key: str) -> Buffer:
@@ -168,7 +176,11 @@ async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None:
168176
"anon": False,
169177
}
170178

171-
meta = {"attributes": {"key": "value"}, "zarr_format": 3, "node_type": "group"}
179+
meta: dict[str, JSON] = {
180+
"attributes": {"key": "value"},
181+
"zarr_format": 3,
182+
"node_type": "group",
183+
}
172184

173185
await store.set(
174186
"zarr.json",
@@ -179,7 +191,7 @@ async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None:
179191
)
180192
assert dict(group.attrs) == {"key": "value"}
181193

182-
meta["attributes"]["key"] = "value-2"
194+
meta["attributes"]["key"] = "value-2" # type: ignore[index]
183195
await store.set(
184196
"directory-2/zarr.json",
185197
self.buffer_cls.from_bytes(json.dumps(meta).encode()),
@@ -189,7 +201,7 @@ async def test_fsspec_store_from_uri(self, store: FsspecStore) -> None:
189201
)
190202
assert dict(group.attrs) == {"key": "value-2"}
191203

192-
meta["attributes"]["key"] = "value-3"
204+
meta["attributes"]["key"] = "value-3" # type: ignore[index]
193205
await store.set(
194206
"directory-3/zarr.json",
195207
self.buffer_cls.from_bytes(json.dumps(meta).encode()),
@@ -216,7 +228,7 @@ def test_from_upath(self) -> None:
216228
assert result.fs.asynchronous
217229
assert result.path == f"{test_bucket_name}/foo/bar"
218230

219-
def test_init_raises_if_path_has_scheme(self, store_kwargs) -> None:
231+
def test_init_raises_if_path_has_scheme(self, store_kwargs: dict[str, Any]) -> None:
220232
# regression test for https://github.com/zarr-developers/zarr-python/issues/2342
221233
store_kwargs["path"] = "s3://" + store_kwargs["path"]
222234
with pytest.raises(
@@ -237,7 +249,7 @@ def test_init_warns_if_fs_asynchronous_is_false(self) -> None:
237249
with pytest.warns(UserWarning, match=r".* was not created with `asynchronous=True`.*"):
238250
self.store_cls(**store_kwargs)
239251

240-
async def test_empty_nonexistent_path(self, store_kwargs) -> None:
252+
async def test_empty_nonexistent_path(self, store_kwargs: dict[str, Any]) -> None:
241253
# regression test for https://github.com/zarr-developers/zarr-python/pull/2343
242254
store_kwargs["path"] += "/abc"
243255
store = await self.store_cls.open(**store_kwargs)
@@ -256,7 +268,7 @@ async def test_delete_dir_unsupported_deletes(self, store: FsspecStore) -> None:
256268
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
257269
reason="No AsyncFileSystemWrapper",
258270
)
259-
def test_wrap_sync_filesystem():
271+
def test_wrap_sync_filesystem() -> None:
260272
"""The local fs is not async so we should expect it to be wrapped automatically"""
261273
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
262274

@@ -270,7 +282,7 @@ def test_wrap_sync_filesystem():
270282
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
271283
reason="No AsyncFileSystemWrapper",
272284
)
273-
def test_no_wrap_async_filesystem():
285+
def test_no_wrap_async_filesystem() -> None:
274286
"""An async fs should not be wrapped automatically; fsspec's https filesystem is such an fs"""
275287
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
276288

@@ -284,12 +296,12 @@ def test_no_wrap_async_filesystem():
284296
parse_version(fsspec.__version__) < parse_version("2024.12.0"),
285297
reason="No AsyncFileSystemWrapper",
286298
)
287-
async def test_delete_dir_wrapped_filesystem(tmpdir) -> None:
299+
async def test_delete_dir_wrapped_filesystem(tmp_path: Path) -> None:
288300
from fsspec.implementations.asyn_wrapper import AsyncFileSystemWrapper
289301
from fsspec.implementations.local import LocalFileSystem
290302

291303
wrapped_fs = AsyncFileSystemWrapper(LocalFileSystem(auto_mkdir=True))
292-
store = FsspecStore(wrapped_fs, read_only=False, path=f"{tmpdir}/test/path")
304+
store = FsspecStore(wrapped_fs, read_only=False, path=f"{tmp_path}/test/path")
293305

294306
assert isinstance(store.fs, AsyncFileSystemWrapper)
295307
assert store.fs.asynchronous

0 commit comments

Comments
 (0)