Skip to content

Commit a94e53c

Browse files
committed
Merge branch 'main' of https://github.com/zarr-developers/zarr-python into feature/store_erase_prefix
2 parents 858d4fb + 8a33df7 commit a94e53c

File tree

9 files changed

+182
-27
lines changed

9 files changed

+182
-27
lines changed

.github/workflows/test.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,35 @@ jobs:
4343
- name: Run Tests
4444
run: |
4545
hatch env run --env test.py${{ matrix.python-version }}-${{ matrix.numpy-version }}-${{ matrix.dependency-set }} run
46+
47+
test-upstream-and-min-deps:
48+
name: py=${{ matrix.python-version }}-${{ matrix.dependency-set }}
49+
50+
runs-on: ubuntu-latest
51+
strategy:
52+
matrix:
53+
python-version: ['3.11', "3.13"]
54+
dependency-set: ["upstream", "min_deps"]
55+
exclude:
56+
- python-version: "3.13"
57+
dependency-set: min_deps
58+
- python-version: "3.11"
59+
dependency-set: upstream
60+
steps:
61+
- uses: actions/checkout@v4
62+
- name: Set up Python
63+
uses: actions/setup-python@v5
64+
with:
65+
python-version: ${{ matrix.python-version }}
66+
cache: 'pip'
67+
- name: Install Hatch
68+
run: |
69+
python -m pip install --upgrade pip
70+
pip install hatch
71+
- name: Set Up Hatch Env
72+
run: |
73+
hatch env create ${{ matrix.dependency-set }}
74+
hatch env run -e ${{ matrix.dependency-set }} list-env
75+
- name: Run Tests
76+
run: |
77+
hatch env run --env ${{ matrix.dependency-set }} run

.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

pyproject.toml

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ requires-python = ">=3.11"
2828
dependencies = [
2929
'asciitree',
3030
'numpy>=1.25',
31-
'numcodecs>=0.10.2',
32-
'fsspec>2024',
33-
'crc32c',
34-
'typing_extensions',
35-
'donfig',
31+
'numcodecs>=0.13',
32+
'fsspec>=2022.10.0',
33+
'crc32c>=2.3',
34+
'typing_extensions>=4.6',
35+
'donfig>=0.8',
3636
]
37+
3738
dynamic = [
3839
"version",
3940
]
@@ -98,7 +99,7 @@ extra = [
9899
]
99100
optional = [
100101
'lmdb',
101-
'universal-pathlib',
102+
'universal-pathlib>=0.0.22',
102103
]
103104

104105
[project.urls]
@@ -183,6 +184,65 @@ features = ['docs']
183184
build = "cd docs && make html"
184185
serve = "sphinx-autobuild docs docs/_build --host 0.0.0.0"
185186

187+
[tool.hatch.envs.upstream]
188+
python = "3.13"
189+
dependencies = [
190+
'numpy', # from scientific-python-nightly-wheels
191+
'numcodecs @ git+https://github.com/zarr-developers/numcodecs',
192+
'fsspec @ git+https://github.com/fsspec/filesystem_spec',
193+
's3fs @ git+https://github.com/fsspec/s3fs',
194+
'universal_pathlib @ git+https://github.com/fsspec/universal_pathlib',
195+
'crc32c @ git+https://github.com/ICRAR/crc32c',
196+
'typing_extensions @ git+https://github.com/python/typing_extensions',
197+
'donfig @ git+https://github.com/pytroll/donfig',
198+
# test deps
199+
'hypothesis',
200+
'pytest',
201+
'pytest-cov',
202+
'pytest-asyncio',
203+
'moto[s3]',
204+
]
205+
206+
[tool.hatch.envs.upstream.env-vars]
207+
PIP_INDEX_URL = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
208+
PIP_EXTRA_INDEX_URL = "https://pypi.org/simple/"
209+
PIP_PRE = "1"
210+
211+
[tool.hatch.envs.upstream.scripts]
212+
run = "pytest --verbose"
213+
run-mypy = "mypy src"
214+
run-hypothesis = "pytest --hypothesis-profile ci tests/test_properties.py tests/test_store/test_stateful*"
215+
list-env = "pip list"
216+
217+
[tool.hatch.envs.min_deps]
218+
description = """Test environment for minimum supported dependencies
219+
220+
See Spec 0000 for details and drop schedule: https://scientific-python.org/specs/spec-0000/
221+
"""
222+
python = "3.11"
223+
dependencies = [
224+
'numpy==1.25.*',
225+
'numcodecs==0.13.*', # 0.13 needed for? (should be 0.11)
226+
'fsspec==2022.10.0',
227+
's3fs==2022.10.0',
228+
'universal_pathlib==0.0.22',
229+
'crc32c==2.3.*',
230+
'typing_extensions==4.6.*', # 4.5 needed for @deprecated, 4.6 for Buffer
231+
'donfig==0.8.*',
232+
# test deps
233+
'hypothesis',
234+
'pytest',
235+
'pytest-cov',
236+
'pytest-asyncio',
237+
'moto[s3]',
238+
]
239+
240+
[tool.hatch.envs.min_deps.scripts]
241+
run = "pytest --verbose"
242+
run-hypothesis = "pytest --hypothesis-profile ci tests/test_properties.py tests/test_store/test_stateful*"
243+
list-env = "pip list"
244+
245+
186246
[tool.ruff]
187247
line-length = 100
188248
force-exclude = true

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/codecs/zstd.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import asyncio
44
from dataclasses import dataclass
55
from functools import cached_property
6-
from importlib.metadata import version
76
from typing import TYPE_CHECKING
87

8+
import numcodecs
99
from numcodecs.zstd import Zstd
10+
from packaging.version import Version
1011

1112
from zarr.abc.codec import BytesBytesCodec
1213
from zarr.core.buffer.cpu import as_numpy_array_wrapper
@@ -43,8 +44,8 @@ class ZstdCodec(BytesBytesCodec):
4344

4445
def __init__(self, *, level: int = 0, checksum: bool = False) -> None:
4546
# numcodecs 0.13.0 introduces the checksum attribute for the zstd codec
46-
_numcodecs_version = tuple(map(int, version("numcodecs").split(".")))
47-
if _numcodecs_version < (0, 13, 0): # pragma: no cover
47+
_numcodecs_version = Version(numcodecs.__version__)
48+
if _numcodecs_version < Version("0.13.0"):
4849
raise RuntimeError(
4950
"numcodecs version >= 0.13.0 is required to use the zstd codec. "
5051
f"Version {_numcodecs_version} is currently installed."

src/zarr/storage/remote.py

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
from __future__ import annotations
22

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

5-
import fsspec
6-
76
from zarr.abc.store import ByteRangeRequest, Store
87
from zarr.storage.common import _dereference_path
98

@@ -34,7 +33,8 @@ class RemoteStore(Store):
3433
mode : AccessModeLiteral
3534
The access mode to use.
3635
path : str
37-
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.
3838
allowed_exceptions : tuple[type[Exception], ...]
3939
When fetching data, these cases will be deemed to correspond to missing keys.
4040
@@ -46,6 +46,23 @@ class RemoteStore(Store):
4646
supports_deletes
4747
supports_partial_writes
4848
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
4966
"""
5067

5168
# based on FSSpec
@@ -71,6 +88,15 @@ def __init__(
7188

7289
if not self.fs.async_impl:
7390
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}://)")
74100

75101
@classmethod
76102
def from_upath(
@@ -130,7 +156,23 @@ def from_url(
130156
-------
131157
RemoteStore
132158
"""
133-
fs, path = fsspec.url_to_fs(url, **storage_options)
159+
try:
160+
from fsspec import url_to_fs
161+
except ImportError:
162+
# before fsspec==2024.3.1
163+
from fsspec.core import url_to_fs
164+
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+
134176
return cls(fs=fs, path=path, mode=mode, allowed_exceptions=allowed_exceptions)
135177

136178
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)