Skip to content

Commit 0045351

Browse files
committed
revive remote store test fixture, and parameterize api tests with all the stores
1 parent e309415 commit 0045351

File tree

3 files changed

+196
-139
lines changed

3 files changed

+196
-139
lines changed

tests/conftest.py

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
from __future__ import annotations
22

3+
import os
34
import pathlib
45
from dataclasses import dataclass, field
56
from typing import TYPE_CHECKING
67

8+
import botocore
9+
import fsspec
710
import numpy as np
811
import numpy.typing as npt
912
import pytest
13+
from botocore.session import Session
1014
from hypothesis import HealthCheck, Verbosity, settings
1115

1216
from zarr import AsyncGroup, config
@@ -23,18 +27,30 @@
2327

2428
from zarr.core.common import ChunkCoords, MemoryOrder, ZarrFormat
2529

30+
s3fs = pytest.importorskip("s3fs")
31+
requests = pytest.importorskip("requests")
32+
moto_server = pytest.importorskip("moto.moto_server.threaded_moto_server")
33+
moto = pytest.importorskip("moto")
34+
35+
# ### amended from s3fs ### #
36+
test_bucket_name = "test"
37+
secure_bucket_name = "test-secure"
38+
port = 5555
39+
endpoint_url = f"http://127.0.0.1:{port}/"
40+
2641

2742
async def parse_store(
2843
store: Literal["local", "memory", "remote", "zip"], path: str
2944
) -> LocalStore | MemoryStore | RemoteStore | ZipStore:
3045
if store == "local":
31-
return await LocalStore.open(path, mode="w")
46+
return await LocalStore.open(path, mode="a")
3247
if store == "memory":
33-
return await MemoryStore.open(mode="w")
48+
return await MemoryStore.open(mode="a")
3449
if store == "remote":
35-
return await RemoteStore.open(url=path, mode="w")
50+
fs = fsspec.filesystem("s3", endpoint_url=endpoint_url, anon=False, asynchronous=True)
51+
return await RemoteStore.open(fs, mode="a")
3652
if store == "zip":
37-
return await ZipStore.open(path + "/zarr.zip", mode="w")
53+
return await ZipStore.open(path + "/zarr.zip", mode="a")
3854
raise AssertionError
3955

4056

@@ -50,26 +66,6 @@ async def store_path(tmpdir: LEGACY_PATH) -> StorePath:
5066
return StorePath(store)
5167

5268

53-
@pytest.fixture
54-
async def local_store(tmpdir: LEGACY_PATH) -> LocalStore:
55-
return await LocalStore.open(str(tmpdir), mode="w")
56-
57-
58-
@pytest.fixture
59-
async def remote_store(url: str) -> RemoteStore:
60-
return await RemoteStore.open(url, mode="w")
61-
62-
63-
@pytest.fixture
64-
async def memory_store() -> MemoryStore:
65-
return await MemoryStore.open(mode="w")
66-
67-
68-
@pytest.fixture
69-
async def zip_store(tmpdir: LEGACY_PATH) -> ZipStore:
70-
return await ZipStore.open(str(tmpdir / "zarr.zip"), mode="w")
71-
72-
7369
@pytest.fixture
7470
async def store(request: pytest.FixtureRequest, tmpdir: LEGACY_PATH) -> Store:
7571
param = request.param
@@ -148,6 +144,54 @@ def zarr_format(request: pytest.FixtureRequest) -> ZarrFormat:
148144
raise ValueError(msg)
149145

150146

147+
@pytest.fixture(scope="module")
148+
def s3_base() -> Generator[None, None, None]:
149+
# writable local S3 system
150+
151+
# This fixture is module-scoped, meaning that we can reuse the MotoServer across all tests
152+
server = moto_server.ThreadedMotoServer(ip_address="127.0.0.1", port=port)
153+
server.start()
154+
if "AWS_SECRET_ACCESS_KEY" not in os.environ:
155+
os.environ["AWS_SECRET_ACCESS_KEY"] = "foo"
156+
if "AWS_ACCESS_KEY_ID" not in os.environ:
157+
os.environ["AWS_ACCESS_KEY_ID"] = "foo"
158+
159+
yield
160+
server.stop()
161+
162+
163+
def get_boto3_client() -> botocore.client.BaseClient:
164+
# NB: we use the sync botocore client for setup
165+
session = Session()
166+
return session.create_client("s3", endpoint_url=endpoint_url, region_name="us-east-1")
167+
168+
169+
@pytest.fixture(autouse=True)
170+
def s3(s3_base: None) -> Generator[s3fs.S3FileSystem, None, None]:
171+
"""
172+
Quoting Martin Durant:
173+
pytest-asyncio creates a new event loop for each async test.
174+
When an async-mode s3fs instance is made from async, it will be assigned to the loop from
175+
which it is made. That means that if you use s3fs again from a subsequent test,
176+
you will have the same identical instance, but be running on a different loop - which fails.
177+
178+
For the rest: it's very convenient to clean up the state of the store between tests,
179+
make sure we start off blank each time.
180+
181+
https://github.com/zarr-developers/zarr-python/pull/1785#discussion_r1634856207
182+
"""
183+
client = get_boto3_client()
184+
client.create_bucket(Bucket=test_bucket_name, ACL="public-read")
185+
s3fs.S3FileSystem.clear_instance_cache()
186+
s3 = s3fs.S3FileSystem(anon=False, client_kwargs={"endpoint_url": endpoint_url})
187+
session = sync(s3.set_session())
188+
s3.invalidate_cache()
189+
yield s3
190+
requests.post(f"{endpoint_url}/moto-api/reset")
191+
client.close()
192+
sync(session.close())
193+
194+
151195
settings.register_profile(
152196
"ci",
153197
max_examples=1000,

tests/test_api.py

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import pathlib
21
import warnings
32
from typing import Literal
43

@@ -27,9 +26,8 @@
2726
from zarr.storage.memory import MemoryStore
2827

2928

30-
def test_create_array(memory_store: Store) -> None:
31-
store = memory_store
32-
29+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
30+
def test_create_array(store: Store) -> None:
3331
# create array
3432
z = create(shape=100, store=store)
3533
assert isinstance(z, Array)
@@ -48,23 +46,23 @@ def test_create_array(memory_store: Store) -> None:
4846
assert z.chunks == (40,)
4947

5048

49+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
5150
@pytest.mark.parametrize("path", ["foo", "/", "/foo", "///foo/bar"])
5251
@pytest.mark.parametrize("node_type", ["array", "group"])
5352
def test_open_normalized_path(
54-
memory_store: MemoryStore, path: str, node_type: Literal["array", "group"]
53+
store: Store, path: str, node_type: Literal["array", "group"]
5554
) -> None:
5655
node: Group | Array
5756
if node_type == "group":
58-
node = group(store=memory_store, path=path)
57+
node = group(store=store, path=path)
5958
elif node_type == "array":
60-
node = create(store=memory_store, path=path, shape=(2,))
59+
node = create(store=store, path=path, shape=(2,))
6160

6261
assert node.path == normalize_path(path)
6362

6463

65-
async def test_open_array(memory_store: MemoryStore) -> None:
66-
store = memory_store
67-
64+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
65+
async def test_open_array(store: Store) -> None:
6866
# open array, create if doesn't exist
6967
z = open(store=store, shape=100)
7068
assert isinstance(z, Array)
@@ -90,9 +88,8 @@ async def test_open_array(memory_store: MemoryStore) -> None:
9088
open(store="doesnotexist", mode="r")
9189

9290

93-
async def test_open_group(memory_store: MemoryStore) -> None:
94-
store = memory_store
95-
91+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
92+
async def test_open_group(store: Store) -> None:
9693
# open group, create if doesn't exist
9794
g = open_group(store=store)
9895
g.create_group("foo")
@@ -105,26 +102,44 @@ async def test_open_group(memory_store: MemoryStore) -> None:
105102
# assert "foo" not in g
106103

107104
# open group, read-only
108-
store_cls = type(store)
109-
ro_store = await store_cls.open(store_dict=store._store_dict, mode="r")
105+
ro_store = store.with_mode("r")
110106
g = open_group(store=ro_store)
111107
assert isinstance(g, Group)
112108
# assert g.read_only
113109

114110

111+
@pytest.mark.parametrize(
112+
"store",
113+
["local", "memory", "remote", pytest.param("zip", marks=pytest.mark.xfail)],
114+
indirect=True,
115+
)
116+
async def test_open_array_or_group(zarr_format: ZarrFormat, store: Store) -> None:
117+
# create a group and an array
118+
grp_attrs = {"foo": "bar"}
119+
grp_w = group(store=store, path="group", zarr_format=zarr_format, attributes=grp_attrs)
120+
arr_w = grp_w.create_array(name="foo", shape=(1,))
121+
122+
grp_r = open(store=store, path="group", mode="r", zarr_format=zarr_format)
123+
assert isinstance(grp_r, Group)
124+
assert grp_r.attrs == grp_attrs
125+
126+
arr_r = open(store=store, path="group/foo", mode="r", zarr_format=zarr_format)
127+
assert isinstance(arr_r, Array)
128+
assert arr_r.shape == arr_w.shape
129+
130+
131+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
115132
@pytest.mark.parametrize("zarr_format", [None, 2, 3])
116-
async def test_open_group_unspecified_version(
117-
tmpdir: pathlib.Path, zarr_format: ZarrFormat
118-
) -> None:
133+
async def test_open_group_unspecified_version(store: Store, zarr_format: ZarrFormat) -> None:
119134
"""Regression test for https://github.com/zarr-developers/zarr-python/issues/2175"""
120135

121136
# create a group with specified zarr format (could be 2, 3, or None)
122137
_ = await zarr.api.asynchronous.open_group(
123-
store=str(tmpdir), mode="w", zarr_format=zarr_format, attributes={"foo": "bar"}
138+
store=store, mode="w", zarr_format=zarr_format, attributes={"foo": "bar"}
124139
)
125140

126141
# now open that group without specifying the format
127-
g2 = await zarr.api.asynchronous.open_group(store=str(tmpdir), mode="r")
142+
g2 = await zarr.api.asynchronous.open_group(store, mode="r")
128143

129144
assert g2.attrs == {"foo": "bar"}
130145

@@ -144,66 +159,71 @@ def test_save_errors() -> None:
144159
save("data/group.zarr")
145160

146161

147-
def test_open_with_mode_r(tmp_path: pathlib.Path) -> None:
162+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
163+
def test_open_with_mode_r(store: Store) -> None:
148164
# 'r' means read only (must exist)
149165
with pytest.raises(FileNotFoundError):
150-
zarr.open(store=tmp_path, mode="r")
151-
z1 = zarr.ones(store=tmp_path, shape=(3, 3))
166+
zarr.open(store=store, mode="r")
167+
z1 = zarr.ones(store=store, shape=(3, 3))
152168
assert z1.fill_value == 1
153-
z2 = zarr.open(store=tmp_path, mode="r")
169+
z2 = zarr.open(store=store, mode="r")
154170
assert isinstance(z2, Array)
155171
assert z2.fill_value == 1
156172
assert (z2[:] == 1).all()
157173
with pytest.raises(ValueError):
158174
z2[:] = 3
159175

160176

161-
def test_open_with_mode_r_plus(tmp_path: pathlib.Path) -> None:
177+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
178+
def test_open_with_mode_r_plus(store: Store) -> None:
162179
# 'r+' means read/write (must exist)
163180
with pytest.raises(FileNotFoundError):
164-
zarr.open(store=tmp_path, mode="r+")
165-
zarr.ones(store=tmp_path, shape=(3, 3))
166-
z2 = zarr.open(store=tmp_path, mode="r+")
181+
zarr.open(store=store, mode="r+")
182+
zarr.ones(store=store, shape=(3, 3))
183+
z2 = zarr.open(store=store, mode="r+")
167184
assert isinstance(z2, Array)
168185
assert (z2[:] == 1).all()
169186
z2[:] = 3
170187

171188

172-
async def test_open_with_mode_a(tmp_path: pathlib.Path) -> None:
189+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
190+
async def test_open_with_mode_a(store: Store) -> None:
173191
# Open without shape argument should default to group
174-
g = zarr.open(store=tmp_path, mode="a")
192+
g = zarr.open(store=store, mode="a")
175193
assert isinstance(g, Group)
176194
await g.store_path.delete()
177195

178196
# 'a' means read/write (create if doesn't exist)
179-
arr = zarr.open(store=tmp_path, mode="a", shape=(3, 3))
197+
arr = zarr.open(store=store, mode="a", shape=(3, 3))
180198
assert isinstance(arr, Array)
181199
arr[...] = 1
182-
z2 = zarr.open(store=tmp_path, mode="a")
200+
z2 = zarr.open(store=store, mode="a")
183201
assert isinstance(z2, Array)
184202
assert (z2[:] == 1).all()
185203
z2[:] = 3
186204

187205

188-
def test_open_with_mode_w(tmp_path: pathlib.Path) -> None:
206+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
207+
def test_open_with_mode_w(store: Store) -> None:
189208
# 'w' means create (overwrite if exists);
190-
arr = zarr.open(store=tmp_path, mode="w", shape=(3, 3))
209+
arr = zarr.open(store=store, mode="w", shape=(3, 3))
191210
assert isinstance(arr, Array)
192211

193212
arr[...] = 3
194-
z2 = zarr.open(store=tmp_path, mode="w", shape=(3, 3))
213+
z2 = zarr.open(store=store, mode="w", shape=(3, 3))
195214
assert isinstance(z2, Array)
196215
assert not (z2[:] == 3).all()
197216
z2[:] = 3
198217

199218

200-
def test_open_with_mode_w_minus(tmp_path: pathlib.Path) -> None:
219+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
220+
def test_open_with_mode_w_minus(store: Store) -> None:
201221
# 'w-' means create (fail if exists)
202-
arr = zarr.open(store=tmp_path, mode="w-", shape=(3, 3))
222+
arr = zarr.open(store=store, mode="w-", shape=(3, 3))
203223
assert isinstance(arr, Array)
204224
arr[...] = 1
205225
with pytest.raises(FileExistsError):
206-
zarr.open(store=tmp_path, mode="w-")
226+
zarr.open(store=store, mode="w-")
207227

208228

209229
@pytest.mark.parametrize("order", ["C", "F", None])
@@ -238,8 +258,8 @@ def test_array_order(order: MemoryOrder | None, zarr_format: ZarrFormat) -> None
238258
# assert "LazyLoader: " in repr(loader)
239259

240260

241-
def test_load_array(memory_store: Store) -> None:
242-
store = memory_store
261+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
262+
def test_load_array(store: Store) -> None:
243263
foo = np.arange(100)
244264
bar = np.arange(100, 0, -1)
245265
save(store, foo=foo, bar=bar)
@@ -967,19 +987,19 @@ def test_open_group_positional_args_deprecated() -> None:
967987
open_group(store, "w")
968988

969989

970-
def test_open_falls_back_to_open_group() -> None:
990+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
991+
def test_open_falls_back_to_open_group(store: Store) -> None:
971992
# https://github.com/zarr-developers/zarr-python/issues/2309
972-
store = MemoryStore(mode="w")
973993
zarr.open_group(store, attributes={"key": "value"})
974994

975995
group = zarr.open(store)
976996
assert isinstance(group, Group)
977997
assert group.attrs == {"key": "value"}
978998

979999

980-
async def test_open_falls_back_to_open_group_async() -> None:
1000+
@pytest.mark.parametrize("store", ["local", "memory", "remote", "zip"], indirect=True)
1001+
async def test_open_falls_back_to_open_group_async(store: Store) -> None:
9811002
# https://github.com/zarr-developers/zarr-python/issues/2309
982-
store = MemoryStore(mode="w")
9831003
await zarr.api.asynchronous.open_group(store, attributes={"key": "value"})
9841004

9851005
group = await zarr.api.asynchronous.open(store=store)

0 commit comments

Comments
 (0)