Skip to content

Commit e37b52f

Browse files
committed
Merge branch 'main' of https://github.com/zarr-developers/zarr-python into ci/expand-ci-matrix
2 parents df24e61 + 8a33df7 commit e37b52f

File tree

6 files changed

+74
-16
lines changed

6 files changed

+74
-16
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ default_language_version:
77
python: python3
88
repos:
99
- repo: https://github.com/astral-sh/ruff-pre-commit
10-
rev: v0.6.9
10+
rev: v0.7.0
1111
hooks:
1212
- id: ruff
1313
args: ["--fix", "--show-fixes"]
@@ -22,7 +22,7 @@ repos:
2222
hooks:
2323
- id: check-yaml
2424
- repo: https://github.com/pre-commit/mirrors-mypy
25-
rev: v1.11.2
25+
rev: v1.12.1
2626
hooks:
2727
- id: mypy
2828
files: src|tests

docs/guide/storage.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ that implements the `AbstractFileSystem` API,
7272
.. code-block:: python
7373
7474
>>> import zarr
75-
>>> store = zarr.storage.RemoteStore("gs://foo/bar", mode="r")
75+
>>> store = zarr.storage.RemoteStore.from_url("gs://foo/bar", mode="r")
7676
>>> zarr.open(store=store)
7777
<Array <RemoteStore(GCSFileSystem, foo/bar)> shape=(10, 20) dtype=float32>
7878

src/zarr/abc/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def with_mode(self, mode: AccessModeLiteral) -> Self:
168168
169169
Returns
170170
-------
171-
store:
171+
store
172172
A new store of the same type with the new mode.
173173
174174
Examples

src/zarr/storage/remote.py

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

3+
import warnings
34
from typing import TYPE_CHECKING, Any, Self
45

56
from zarr.abc.store import ByteRangeRequest, Store
@@ -32,7 +33,8 @@ class RemoteStore(Store):
3233
mode : AccessModeLiteral
3334
The access mode to use.
3435
path : str
35-
The root path of the store.
36+
The root path of the store. This should be a relative path and must not include the
37+
filesystem scheme.
3638
allowed_exceptions : tuple[type[Exception], ...]
3739
When fetching data, these cases will be deemed to correspond to missing keys.
3840
@@ -44,6 +46,23 @@ class RemoteStore(Store):
4446
supports_deletes
4547
supports_partial_writes
4648
supports_listing
49+
50+
Raises
51+
------
52+
TypeError
53+
If the Filesystem does not support async operations.
54+
ValueError
55+
If the path argument includes a scheme.
56+
57+
Warns
58+
-----
59+
UserWarning
60+
If the file system (fs) was not created with `asynchronous=True`.
61+
62+
See Also
63+
--------
64+
RemoteStore.from_upath
65+
RemoteStore.from_url
4766
"""
4867

4968
# based on FSSpec
@@ -69,6 +88,15 @@ def __init__(
6988

7089
if not self.fs.async_impl:
7190
raise TypeError("Filesystem needs to support async operations.")
91+
if not self.fs.asynchronous:
92+
warnings.warn(
93+
f"fs ({fs}) was not created with `asynchronous=True`, this may lead to surprising behavior",
94+
stacklevel=2,
95+
)
96+
if "://" in path and not path.startswith("http"):
97+
# `not path.startswith("http")` is a special case for the http filesystem (¯\_(ツ)_/¯)
98+
scheme, _ = path.split("://", maxsplit=1)
99+
raise ValueError(f"path argument to RemoteStore must not include scheme ({scheme}://)")
72100

73101
@classmethod
74102
def from_upath(
@@ -134,7 +162,17 @@ def from_url(
134162
# before fsspec==2024.3.1
135163
from fsspec.core import url_to_fs
136164

137-
fs, path = url_to_fs(url, **storage_options)
165+
opts = storage_options or {}
166+
opts = {"asynchronous": True, **opts}
167+
168+
fs, path = url_to_fs(url, **opts)
169+
170+
# fsspec is not consistent about removing the scheme from the path, so check and strip it here
171+
# https://github.com/fsspec/filesystem_spec/issues/1722
172+
if "://" in path and not path.startswith("http"):
173+
# `not path.startswith("http")` is a special case for the http filesystem (¯\_(ツ)_/¯)
174+
path = fs._strip_protocol(path)
175+
138176
return cls(fs=fs, path=path, mode=mode, allowed_exceptions=allowed_exceptions)
139177

140178
async def clear(self) -> None:

tests/test_store/test_core.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,7 @@ async def test_make_store_path_invalid() -> None:
7171

7272

7373
async def test_make_store_path_fsspec(monkeypatch) -> None:
74-
import fsspec.implementations.memory
75-
76-
monkeypatch.setattr(fsspec.implementations.memory.MemoryFileSystem, "async_impl", True)
77-
store_path = await make_store_path("memory://")
74+
store_path = await make_store_path("http://foo.com/bar")
7875
assert isinstance(store_path.store, RemoteStore)
7976

8077

tests/test_store/test_remote.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@ def s3(s3_base: None) -> Generator[s3fs.S3FileSystem, None, None]:
8686

8787
async def test_basic() -> None:
8888
store = RemoteStore.from_url(
89-
f"s3://{test_bucket_name}",
89+
f"s3://{test_bucket_name}/foo/spam/",
9090
mode="w",
9191
storage_options={"endpoint_url": endpoint_url, "anon": False},
9292
)
93+
assert store.fs.asynchronous
94+
assert store.path == f"{test_bucket_name}/foo/spam"
9395
assert await _collect_aiterator(store.list()) == ()
9496
assert not await store.exists("foo")
9597
data = b"hello"
@@ -109,7 +111,7 @@ class TestRemoteStoreS3(StoreTests[RemoteStore, cpu.Buffer]):
109111
@pytest.fixture
110112
def store_kwargs(self, request) -> dict[str, str | bool]:
111113
fs, path = fsspec.url_to_fs(
112-
f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False
114+
f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=True
113115
)
114116
return {"fs": fs, "path": path, "mode": "r+"}
115117

@@ -143,9 +145,7 @@ def test_store_supports_partial_writes(self, store: RemoteStore) -> None:
143145
def test_store_supports_listing(self, store: RemoteStore) -> None:
144146
assert store.supports_listing
145147

146-
async def test_remote_store_from_uri(
147-
self, store: RemoteStore, store_kwargs: dict[str, str | bool]
148-
):
148+
async def test_remote_store_from_uri(self, store: RemoteStore):
149149
storage_options = {
150150
"endpoint_url": endpoint_url,
151151
"anon": False,
@@ -183,9 +183,32 @@ async def test_remote_store_from_uri(
183183
assert dict(group.attrs) == {"key": "value-3"}
184184

185185
def test_from_upath(self) -> None:
186-
path = UPath(f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False)
186+
path = UPath(
187+
f"s3://{test_bucket_name}/foo/bar/",
188+
endpoint_url=endpoint_url,
189+
anon=False,
190+
asynchronous=True,
191+
)
187192
result = RemoteStore.from_upath(path)
188193
assert result.fs.endpoint_url == endpoint_url
194+
assert result.fs.asynchronous
195+
assert result.path == f"{test_bucket_name}/foo/bar"
196+
197+
def test_init_raises_if_path_has_scheme(self, store_kwargs) -> None:
198+
# regression test for https://github.com/zarr-developers/zarr-python/issues/2342
199+
store_kwargs["path"] = "s3://" + store_kwargs["path"]
200+
with pytest.raises(
201+
ValueError, match="path argument to RemoteStore must not include scheme .*"
202+
):
203+
self.store_cls(**store_kwargs)
204+
205+
def test_init_warns_if_fs_asynchronous_is_false(self) -> None:
206+
fs, path = fsspec.url_to_fs(
207+
f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=False
208+
)
209+
store_kwargs = {"fs": fs, "path": path, "mode": "r+"}
210+
with pytest.warns(UserWarning, match=r".* was not created with `asynchronous=True`.*"):
211+
self.store_cls(**store_kwargs)
189212

190213
async def test_empty_nonexistent_path(self, store_kwargs) -> None:
191214
# regression test for https://github.com/zarr-developers/zarr-python/pull/2343

0 commit comments

Comments
 (0)