Skip to content

Commit e0751ab

Browse files
dstansbyd-v-b
andauthored
Fix typing in store tests (#3394)
* Fix typing in test_core * Fix typing in test_logging * Fix typing in test_object * Fix typing in test_stateful * Fix typing in test_wrapper * Fix wrapper store tests * Fix store in test * Fix merge conflicts * Adapt ObjectStore tests for new generic class * Fix generic assignment in tests --------- Co-authored-by: Davis Bennett <[email protected]>
1 parent be519b0 commit e0751ab

File tree

7 files changed

+139
-92
lines changed

7 files changed

+139
-92
lines changed

pyproject.toml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,18 +359,18 @@ module = [
359359
"tests.test_store.test_memory",
360360
"tests.test_codecs.test_codecs",
361361
"tests.test_metadata.*",
362+
"tests.test_store.test_core",
363+
"tests.test_store.test_logging",
364+
"tests.test_store.test_object",
365+
"tests.test_store.test_stateful",
366+
"tests.test_store.test_wrapper",
362367
]
363368
strict = false
364369

365370
# TODO: Move the next modules up to the strict = false section
366371
# and fix the errors
367372
[[tool.mypy.overrides]]
368373
module = [
369-
"tests.test_store.test_core",
370-
"tests.test_store.test_logging",
371-
"tests.test_store.test_object",
372-
"tests.test_store.test_stateful",
373-
"tests.test_store.test_wrapper",
374374
"tests.test_group",
375375
"tests.test_indexing",
376376
"tests.test_properties",

src/zarr/storage/_wrapper.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from types import TracebackType
88
from typing import Any, Self
99

10+
from zarr.abc.buffer import Buffer
1011
from zarr.abc.store import ByteRequest
11-
from zarr.core.buffer import Buffer, BufferPrototype
12+
from zarr.core.buffer import BufferPrototype
1213

1314
from zarr.abc.store import Store
1415

tests/test_store/test_core.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import tempfile
2+
from collections.abc import Callable, Generator
23
from pathlib import Path
4+
from typing import Any, Literal
35

46
import pytest
57
from _pytest.compat import LEGACY_PATH
@@ -21,7 +23,9 @@
2123
@pytest.fixture(
2224
params=["none", "temp_dir_str", "temp_dir_path", "store_path", "memory_store", "dict"]
2325
)
24-
def store_like(request):
26+
def store_like(
27+
request: pytest.FixtureRequest,
28+
) -> Generator[None | str | Path | StorePath | MemoryStore | dict[Any, Any], None, None]:
2529
if request.param == "none":
2630
yield None
2731
elif request.param == "temp_dir_str":
@@ -42,7 +46,7 @@ def store_like(request):
4246
@pytest.mark.parametrize("write_group", [True, False])
4347
@pytest.mark.parametrize("zarr_format", [2, 3])
4448
async def test_contains_group(
45-
local_store, path: str, write_group: bool, zarr_format: ZarrFormat
49+
local_store: LocalStore, path: str, write_group: bool, zarr_format: ZarrFormat
4650
) -> None:
4751
"""
4852
Test that the contains_group method correctly reports the existence of a group.
@@ -58,7 +62,7 @@ async def test_contains_group(
5862
@pytest.mark.parametrize("write_array", [True, False])
5963
@pytest.mark.parametrize("zarr_format", [2, 3])
6064
async def test_contains_array(
61-
local_store, path: str, write_array: bool, zarr_format: ZarrFormat
65+
local_store: LocalStore, path: str, write_array: bool, zarr_format: ZarrFormat
6266
) -> None:
6367
"""
6468
Test that the contains array method correctly reports the existence of an array.
@@ -71,13 +75,15 @@ async def test_contains_array(
7175

7276

7377
@pytest.mark.parametrize("func", [contains_array, contains_group])
74-
async def test_contains_invalid_format_raises(local_store, func: callable) -> None:
78+
async def test_contains_invalid_format_raises(
79+
local_store: LocalStore, func: Callable[[Any], Any]
80+
) -> None:
7581
"""
7682
Test contains_group and contains_array raise errors for invalid zarr_formats
7783
"""
7884
store_path = StorePath(local_store)
7985
with pytest.raises(ValueError):
80-
assert await func(store_path, zarr_format="3.0")
86+
assert await func(store_path, zarr_format="3.0") # type: ignore[call-arg]
8187

8288

8389
@pytest.mark.parametrize("path", [None, "", "bar"])
@@ -113,40 +119,48 @@ async def test_make_store_path_local(
113119
@pytest.mark.parametrize("path", [None, "", "bar"])
114120
@pytest.mark.parametrize("mode", ["r", "w"])
115121
async def test_make_store_path_store_path(
116-
tmpdir: LEGACY_PATH, path: str, mode: AccessModeLiteral
122+
tmp_path: Path, path: str, mode: AccessModeLiteral
117123
) -> None:
118124
"""
119125
Test invoking make_store_path when the input is another store_path. In particular we want to ensure
120126
that a new path is handled correctly.
121127
"""
122128
ro = mode == "r"
123-
store_like = await StorePath.open(LocalStore(str(tmpdir), read_only=ro), path="root", mode=mode)
129+
store_like = await StorePath.open(
130+
LocalStore(str(tmp_path), read_only=ro), path="root", mode=mode
131+
)
124132
store_path = await make_store_path(store_like, path=path, mode=mode)
125133
assert isinstance(store_path.store, LocalStore)
126-
assert Path(store_path.store.root) == Path(tmpdir)
134+
assert Path(store_path.store.root) == tmp_path
127135
path_normalized = normalize_path(path)
128136
assert store_path.path == (store_like / path_normalized).path
129137
assert store_path.read_only == ro
130138

131139

132140
@pytest.mark.parametrize("modes", [(True, "w"), (False, "x")])
133-
async def test_store_path_invalid_mode_raises(tmpdir: LEGACY_PATH, modes: tuple) -> None:
141+
async def test_store_path_invalid_mode_raises(
142+
tmp_path: Path, modes: tuple[bool, Literal["w", "x"]]
143+
) -> None:
134144
"""
135145
Test that ValueErrors are raise for invalid mode.
136146
"""
137147
with pytest.raises(ValueError):
138-
await StorePath.open(LocalStore(str(tmpdir), read_only=modes[0]), path=None, mode=modes[1])
148+
await StorePath.open(
149+
LocalStore(str(tmp_path), read_only=modes[0]),
150+
path="",
151+
mode=modes[1], # type:ignore[arg-type]
152+
)
139153

140154

141155
async def test_make_store_path_invalid() -> None:
142156
"""
143157
Test that invalid types raise TypeError
144158
"""
145159
with pytest.raises(TypeError):
146-
await make_store_path(1) # type: ignore[arg-type]
160+
await make_store_path(1)
147161

148162

149-
async def test_make_store_path_fsspec(monkeypatch) -> None:
163+
async def test_make_store_path_fsspec() -> None:
150164
pytest.importorskip("fsspec")
151165
pytest.importorskip("requests")
152166
pytest.importorskip("aiohttp")
@@ -161,7 +175,7 @@ async def test_make_store_path_storage_options_raises(store_like: StoreLike) ->
161175

162176
async def test_unsupported() -> None:
163177
with pytest.raises(TypeError, match="Unsupported type for store_like: 'int'"):
164-
await make_store_path(1) # type: ignore[arg-type]
178+
await make_store_path(1)
165179

166180

167181
@pytest.mark.parametrize(
@@ -184,12 +198,12 @@ def test_normalize_path_upath() -> None:
184198
assert normalize_path(upath.UPath("foo/bar")) == "foo/bar"
185199

186200

187-
def test_normalize_path_none():
201+
def test_normalize_path_none() -> None:
188202
assert normalize_path(None) == ""
189203

190204

191205
@pytest.mark.parametrize("path", [".", ".."])
192-
def test_normalize_path_invalid(path: str):
206+
def test_normalize_path_invalid(path: str) -> None:
193207
with pytest.raises(ValueError):
194208
normalize_path(path)
195209

@@ -230,7 +244,7 @@ def test_invalid(paths: tuple[str, str]) -> None:
230244
_normalize_paths(paths)
231245

232246

233-
def test_normalize_path_keys():
247+
def test_normalize_path_keys() -> None:
234248
"""
235249
Test that ``_normalize_path_keys`` just applies the normalize_path function to each key of its
236250
input
@@ -272,10 +286,10 @@ def test_different_open_mode(tmp_path: LEGACY_PATH) -> None:
272286

273287
# Test with a store that doesn't implement .with_read_only()
274288
zarr_path = tmp_path / "foo.zarr"
275-
store = ZipStore(zarr_path, mode="w")
276-
zarr.create((100,), store=store, zarr_format=2, path="a")
289+
zip_store = ZipStore(zarr_path, mode="w")
290+
zarr.create((100,), store=zip_store, zarr_format=2, path="a")
277291
with pytest.raises(
278292
ValueError,
279293
match="Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store. Please use a read-only store or a storage class that implements .with_read_only().",
280294
):
281-
zarr.open_array(store=store, path="a", zarr_format=2, mode="r")
295+
zarr.open_array(store=zip_store, path="a", zarr_format=2, mode="r")

tests/test_store/test_logging.py

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

33
import logging
4-
from typing import TYPE_CHECKING
4+
from typing import TYPE_CHECKING, TypedDict
55

66
import pytest
77

@@ -11,57 +11,65 @@
1111
from zarr.testing.store import StoreTests
1212

1313
if TYPE_CHECKING:
14-
from _pytest.compat import LEGACY_PATH
14+
from pathlib import Path
1515

1616
from zarr.abc.store import Store
1717

1818

19-
class TestLoggingStore(StoreTests[LoggingStore, cpu.Buffer]):
20-
store_cls = LoggingStore
19+
class StoreKwargs(TypedDict):
20+
store: LocalStore
21+
log_level: str
22+
23+
24+
class TestLoggingStore(StoreTests[LoggingStore[LocalStore], cpu.Buffer]):
25+
# store_cls is needed to do an isintsance check, so can't be a subscripted generic
26+
store_cls = LoggingStore # type: ignore[assignment]
2127
buffer_cls = cpu.Buffer
2228

23-
async def get(self, store: LoggingStore, key: str) -> Buffer:
29+
async def get(self, store: LoggingStore[LocalStore], key: str) -> Buffer:
2430
return self.buffer_cls.from_bytes((store._store.root / key).read_bytes())
2531

26-
async def set(self, store: LoggingStore, key: str, value: Buffer) -> None:
32+
async def set(self, store: LoggingStore[LocalStore], key: str, value: Buffer) -> None:
2733
parent = (store._store.root / key).parent
2834
if not parent.exists():
2935
parent.mkdir(parents=True)
3036
(store._store.root / key).write_bytes(value.to_bytes())
3137

3238
@pytest.fixture
33-
def store_kwargs(self, tmpdir: LEGACY_PATH) -> dict[str, str]:
34-
return {"store": LocalStore(str(tmpdir)), "log_level": "DEBUG"}
39+
def store_kwargs(self, tmp_path: Path) -> StoreKwargs:
40+
return {"store": LocalStore(str(tmp_path)), "log_level": "DEBUG"}
3541

3642
@pytest.fixture
37-
def open_kwargs(self, tmpdir) -> dict[str, str]:
38-
return {"store_cls": LocalStore, "root": str(tmpdir), "log_level": "DEBUG"}
43+
def open_kwargs(self, tmp_path: Path) -> dict[str, type[LocalStore] | str]:
44+
return {"store_cls": LocalStore, "root": str(tmp_path), "log_level": "DEBUG"}
3945

4046
@pytest.fixture
41-
def store(self, store_kwargs: str | dict[str, Buffer] | None) -> LoggingStore:
47+
def store(self, store_kwargs: StoreKwargs) -> LoggingStore[LocalStore]:
4248
return self.store_cls(**store_kwargs)
4349

44-
def test_store_supports_writes(self, store: LoggingStore) -> None:
50+
def test_store_supports_writes(self, store: LoggingStore[LocalStore]) -> None:
4551
assert store.supports_writes
4652

47-
def test_store_supports_listing(self, store: LoggingStore) -> None:
53+
def test_store_supports_listing(self, store: LoggingStore[LocalStore]) -> None:
4854
assert store.supports_listing
4955

50-
def test_store_repr(self, store: LoggingStore) -> None:
56+
def test_store_repr(self, store: LoggingStore[LocalStore]) -> None:
5157
assert f"{store!r}" == f"LoggingStore(LocalStore, 'file://{store._store.root.as_posix()}')"
5258

53-
def test_store_str(self, store: LoggingStore) -> None:
59+
def test_store_str(self, store: LoggingStore[LocalStore]) -> None:
5460
assert str(store) == f"logging-file://{store._store.root.as_posix()}"
5561

56-
async def test_default_handler(self, local_store, capsys) -> None:
62+
async def test_default_handler(
63+
self, local_store: LocalStore, capsys: pytest.CaptureFixture[str]
64+
) -> None:
5765
# Store and then remove existing handlers to enter default handler code path
5866
handlers = logging.getLogger().handlers[:]
5967
for h in handlers:
6068
logging.getLogger().removeHandler(h)
6169
# Test logs are sent to stdout
6270
wrapped = LoggingStore(store=local_store)
6371
buffer = default_buffer_prototype().buffer
64-
res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04"))
72+
res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value]
6573
assert res is None
6674
captured = capsys.readouterr()
6775
assert len(captured) == 2
@@ -71,7 +79,7 @@ async def test_default_handler(self, local_store, capsys) -> None:
7179
for h in handlers:
7280
logging.getLogger().addHandler(h)
7381

74-
def test_is_open_setter_raises(self, store: LoggingStore) -> None:
82+
def test_is_open_setter_raises(self, store: LoggingStore[LocalStore]) -> None:
7583
"Test that a user cannot change `_is_open` without opening the underlying store."
7684
with pytest.raises(
7785
NotImplementedError, match="LoggingStore must be opened via the `_open` method"
@@ -80,12 +88,12 @@ def test_is_open_setter_raises(self, store: LoggingStore) -> None:
8088

8189

8290
@pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"])
83-
async def test_logging_store(store: Store, caplog) -> None:
91+
async def test_logging_store(store: Store, caplog: pytest.LogCaptureFixture) -> None:
8492
wrapped = LoggingStore(store=store, log_level="DEBUG")
8593
buffer = default_buffer_prototype().buffer
8694

8795
caplog.clear()
88-
res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04"))
96+
res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value]
8997
assert res is None
9098
assert len(caplog.record_tuples) == 2
9199
for tup in caplog.record_tuples:

0 commit comments

Comments
 (0)