From 9efebe3b53836ef724a62ae803d9cc1c582e4383 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:08:36 +0100 Subject: [PATCH 01/10] Fix typing in test_core --- pyproject.toml | 2 +- tests/test_store/test_core.py | 50 ++++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 52b032f771..4578431904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -353,6 +353,7 @@ module = [ "tests.test_store.test_memory", "tests.test_codecs.test_codecs", "tests.test_metadata.*", + "tests.test_store.test_core", ] strict = false @@ -360,7 +361,6 @@ strict = false # and fix the errors [[tool.mypy.overrides]] module = [ - "tests.test_store.test_core", "tests.test_store.test_logging", "tests.test_store.test_object", "tests.test_store.test_stateful", diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index a3850de90f..2dfdaa55de 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -1,5 +1,7 @@ import tempfile +from collections.abc import Callable, Generator from pathlib import Path +from typing import Any, Literal import pytest from _pytest.compat import LEGACY_PATH @@ -21,7 +23,9 @@ @pytest.fixture( params=["none", "temp_dir_str", "temp_dir_path", "store_path", "memory_store", "dict"] ) -def store_like(request): +def store_like( + request: pytest.FixtureRequest, +) -> Generator[None | str | Path | StorePath | MemoryStore | dict[Any, Any], None, None]: if request.param == "none": yield None elif request.param == "temp_dir_str": @@ -42,7 +46,7 @@ def store_like(request): @pytest.mark.parametrize("write_group", [True, False]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_contains_group( - local_store, path: str, write_group: bool, zarr_format: ZarrFormat + local_store: LocalStore, path: str, write_group: bool, zarr_format: ZarrFormat ) -> None: """ Test that the contains_group method correctly reports the existence of a group. @@ -58,7 +62,7 @@ async def test_contains_group( @pytest.mark.parametrize("write_array", [True, False]) @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_contains_array( - local_store, path: str, write_array: bool, zarr_format: ZarrFormat + local_store: LocalStore, path: str, write_array: bool, zarr_format: ZarrFormat ) -> None: """ Test that the contains array method correctly reports the existence of an array. @@ -71,13 +75,15 @@ async def test_contains_array( @pytest.mark.parametrize("func", [contains_array, contains_group]) -async def test_contains_invalid_format_raises(local_store, func: callable) -> None: +async def test_contains_invalid_format_raises( + local_store: LocalStore, func: Callable[[Any], Any] +) -> None: """ Test contains_group and contains_array raise errors for invalid zarr_formats """ store_path = StorePath(local_store) with pytest.raises(ValueError): - assert await func(store_path, zarr_format="3.0") + assert await func(store_path, zarr_format="3.0") # type: ignore[call-arg] @pytest.mark.parametrize("path", [None, "", "bar"]) @@ -113,29 +119,37 @@ async def test_make_store_path_local( @pytest.mark.parametrize("path", [None, "", "bar"]) @pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_store_path( - tmpdir: LEGACY_PATH, path: str, mode: AccessModeLiteral + tmp_path: Path, path: str, mode: AccessModeLiteral ) -> None: """ Test invoking make_store_path when the input is another store_path. In particular we want to ensure that a new path is handled correctly. """ ro = mode == "r" - store_like = await StorePath.open(LocalStore(str(tmpdir), read_only=ro), path="root", mode=mode) + store_like = await StorePath.open( + LocalStore(str(tmp_path), read_only=ro), path="root", mode=mode + ) store_path = await make_store_path(store_like, path=path, mode=mode) assert isinstance(store_path.store, LocalStore) - assert Path(store_path.store.root) == Path(tmpdir) + assert Path(store_path.store.root) == tmp_path path_normalized = normalize_path(path) assert store_path.path == (store_like / path_normalized).path assert store_path.read_only == ro @pytest.mark.parametrize("modes", [(True, "w"), (False, "x")]) -async def test_store_path_invalid_mode_raises(tmpdir: LEGACY_PATH, modes: tuple) -> None: +async def test_store_path_invalid_mode_raises( + tmp_path: Path, modes: tuple[bool, Literal["w", "x"]] +) -> None: """ Test that ValueErrors are raise for invalid mode. """ with pytest.raises(ValueError): - await StorePath.open(LocalStore(str(tmpdir), read_only=modes[0]), path=None, mode=modes[1]) + await StorePath.open( + LocalStore(str(tmp_path), read_only=modes[0]), + path="", + mode=modes[1], # type:ignore[arg-type] + ) async def test_make_store_path_invalid() -> None: @@ -143,10 +157,10 @@ async def test_make_store_path_invalid() -> None: Test that invalid types raise TypeError """ with pytest.raises(TypeError): - await make_store_path(1) # type: ignore[arg-type] + await make_store_path(1) -async def test_make_store_path_fsspec(monkeypatch) -> None: +async def test_make_store_path_fsspec() -> None: pytest.importorskip("fsspec") pytest.importorskip("requests") pytest.importorskip("aiohttp") @@ -161,7 +175,7 @@ async def test_make_store_path_storage_options_raises(store_like: StoreLike) -> async def test_unsupported() -> None: with pytest.raises(TypeError, match="Unsupported type for store_like: 'int'"): - await make_store_path(1) # type: ignore[arg-type] + await make_store_path(1) @pytest.mark.parametrize( @@ -184,12 +198,12 @@ def test_normalize_path_upath() -> None: assert normalize_path(upath.UPath("foo/bar")) == "foo/bar" -def test_normalize_path_none(): +def test_normalize_path_none() -> None: assert normalize_path(None) == "" @pytest.mark.parametrize("path", [".", ".."]) -def test_normalize_path_invalid(path: str): +def test_normalize_path_invalid(path: str) -> None: with pytest.raises(ValueError): normalize_path(path) @@ -230,7 +244,7 @@ def test_invalid(paths: tuple[str, str]) -> None: _normalize_paths(paths) -def test_normalize_path_keys(): +def test_normalize_path_keys() -> None: """ Test that ``_normalize_path_keys`` just applies the normalize_path function to each key of its input @@ -272,10 +286,10 @@ def test_different_open_mode(tmp_path: LEGACY_PATH) -> None: # Test with a store that doesn't implement .with_read_only() zarr_path = tmp_path / "foo.zarr" - store = ZipStore(zarr_path, mode="w") + zip_store = ZipStore(zarr_path, mode="w") zarr.create((100,), store=store, zarr_format=2, path="a") with pytest.raises( ValueError, 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().", ): - zarr.open_array(store=store, path="a", zarr_format=2, mode="r") + zarr.open_array(store=zip_store, path="a", zarr_format=2, mode="r") From 1fe910dbdc7e2359a74e1b075353c8ed515b419c Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:19:19 +0100 Subject: [PATCH 02/10] Fix typing in test_logging --- pyproject.toml | 2 +- tests/test_store/test_logging.py | 49 ++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4578431904..4ca3cb2282 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -354,6 +354,7 @@ module = [ "tests.test_codecs.test_codecs", "tests.test_metadata.*", "tests.test_store.test_core", + "tests.test_store.test_logging", ] strict = false @@ -361,7 +362,6 @@ strict = false # and fix the errors [[tool.mypy.overrides]] module = [ - "tests.test_store.test_logging", "tests.test_store.test_object", "tests.test_store.test_stateful", "tests.test_store.test_wrapper", diff --git a/tests/test_store/test_logging.py b/tests/test_store/test_logging.py index 1a89dca874..cc6f72331e 100644 --- a/tests/test_store/test_logging.py +++ b/tests/test_store/test_logging.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypedDict import pytest @@ -11,52 +11,59 @@ from zarr.testing.store import StoreTests if TYPE_CHECKING: - from _pytest.compat import LEGACY_PATH + from pathlib import Path from zarr.abc.store import Store -class TestLoggingStore(StoreTests[LoggingStore, cpu.Buffer]): - store_cls = LoggingStore +class StoreKwargs(TypedDict): + store: LocalStore + log_level: str + + +class TestLoggingStore(StoreTests[LoggingStore[LocalStore], cpu.Buffer]): + store_cls = LoggingStore[LocalStore] buffer_cls = cpu.Buffer - async def get(self, store: LoggingStore, key: str) -> Buffer: + async def get(self, store: LoggingStore[LocalStore], key: str) -> Buffer: return self.buffer_cls.from_bytes((store._store.root / key).read_bytes()) - async def set(self, store: LoggingStore, key: str, value: Buffer) -> None: + async def set(self, store: LoggingStore[LocalStore], key: str, value: Buffer) -> None: parent = (store._store.root / key).parent if not parent.exists(): parent.mkdir(parents=True) (store._store.root / key).write_bytes(value.to_bytes()) @pytest.fixture - def store_kwargs(self, tmpdir: LEGACY_PATH) -> dict[str, str]: - return {"store": LocalStore(str(tmpdir)), "log_level": "DEBUG"} + def store_kwargs(self, tmp_path: Path) -> StoreKwargs: + return {"store": LocalStore(str(tmp_path)), "log_level": "DEBUG"} @pytest.fixture - def open_kwargs(self, tmpdir) -> dict[str, str]: - return {"store_cls": LocalStore, "root": str(tmpdir), "log_level": "DEBUG"} + def open_kwargs(self, tmp_path: Path) -> dict[str, type[LocalStore] | str]: + return {"store_cls": LocalStore, "root": str(tmp_path), "log_level": "DEBUG"} @pytest.fixture - def store(self, store_kwargs: str | dict[str, Buffer] | None) -> LoggingStore: + def store(self, store_kwargs: StoreKwargs) -> LoggingStore[LocalStore]: return self.store_cls(**store_kwargs) - def test_store_supports_writes(self, store: LoggingStore) -> None: + def test_store_supports_writes(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_writes - def test_store_supports_partial_writes(self, store: LoggingStore) -> None: + def test_store_supports_partial_writes(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_partial_writes - def test_store_supports_listing(self, store: LoggingStore) -> None: + def test_store_supports_listing(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_listing - def test_store_repr(self, store: LoggingStore) -> None: + def test_store_repr(self, store: LoggingStore[LocalStore]) -> None: assert f"{store!r}" == f"LoggingStore(LocalStore, 'file://{store._store.root.as_posix()}')" - def test_store_str(self, store: LoggingStore) -> None: + def test_store_str(self, store: LoggingStore[LocalStore]) -> None: assert str(store) == f"logging-file://{store._store.root.as_posix()}" - async def test_default_handler(self, local_store, capsys) -> None: + async def test_default_handler( + self, local_store: LocalStore, capsys: pytest.CaptureFixture[str] + ) -> None: # Store and then remove existing handlers to enter default handler code path handlers = logging.getLogger().handlers[:] for h in handlers: @@ -64,7 +71,7 @@ async def test_default_handler(self, local_store, capsys) -> None: # Test logs are sent to stdout wrapped = LoggingStore(store=local_store) buffer = default_buffer_prototype().buffer - res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) + res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value] assert res is None captured = capsys.readouterr() assert len(captured) == 2 @@ -74,7 +81,7 @@ async def test_default_handler(self, local_store, capsys) -> None: for h in handlers: logging.getLogger().addHandler(h) - def test_is_open_setter_raises(self, store: LoggingStore) -> None: + def test_is_open_setter_raises(self, store: LoggingStore[LocalStore]) -> None: "Test that a user cannot change `_is_open` without opening the underlying store." with pytest.raises( NotImplementedError, match="LoggingStore must be opened via the `_open` method" @@ -83,12 +90,12 @@ def test_is_open_setter_raises(self, store: LoggingStore) -> None: @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=["store"]) -async def test_logging_store(store: Store, caplog) -> None: +async def test_logging_store(store: Store, caplog: pytest.LogCaptureFixture) -> None: wrapped = LoggingStore(store=store, log_level="DEBUG") buffer = default_buffer_prototype().buffer caplog.clear() - res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) + res = await wrapped.set("foo/bar/c/0", buffer.from_bytes(b"\x01\x02\x03\x04")) # type: ignore[func-returns-value] assert res is None assert len(caplog.record_tuples) == 2 for tup in caplog.record_tuples: From 8cfaac84717f2cff648329bb40dd685b116b7aae Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:26:00 +0100 Subject: [PATCH 03/10] Fix typing in test_object --- pyproject.toml | 2 +- tests/test_store/test_object.py | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4ca3cb2282..1b2e6635b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -355,6 +355,7 @@ module = [ "tests.test_metadata.*", "tests.test_store.test_core", "tests.test_store.test_logging", + "tests.test_store.test_object", ] strict = false @@ -362,7 +363,6 @@ strict = false # and fix the errors [[tool.mypy.overrides]] module = [ - "tests.test_store.test_object", "tests.test_store.test_stateful", "tests.test_store.test_wrapper", "tests.test_group", diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index d8b89e56b7..9f5f3aef93 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -1,5 +1,6 @@ # ruff: noqa: E402 -from typing import Any +from pathlib import Path +from typing import TypedDict import pytest @@ -16,17 +17,22 @@ from zarr.testing.store import StoreTests +class StoreKwargs(TypedDict): + store: LocalStore + read_only: bool + + class TestObjectStore(StoreTests[ObjectStore, cpu.Buffer]): store_cls = ObjectStore buffer_cls = cpu.Buffer @pytest.fixture - def store_kwargs(self, tmpdir) -> dict[str, Any]: - store = LocalStore(prefix=tmpdir) + def store_kwargs(self, tmp_path: Path) -> StoreKwargs: + store = LocalStore(prefix=tmp_path) return {"store": store, "read_only": False} @pytest.fixture - def store(self, store_kwargs: dict[str, str | bool]) -> ObjectStore: + def store(self, store_kwargs: StoreKwargs) -> ObjectStore: return self.store_cls(**store_kwargs) async def get(self, store: ObjectStore, key: str) -> Buffer: @@ -48,10 +54,8 @@ def test_store_repr(self, store: ObjectStore) -> None: def test_store_supports_writes(self, store: ObjectStore) -> None: assert store.supports_writes - async def test_store_supports_partial_writes(self, store: ObjectStore) -> None: + def test_store_supports_partial_writes(self, store: ObjectStore) -> None: assert not store.supports_partial_writes - with pytest.raises(NotImplementedError): - await store.set_partial_values([("foo", 0, b"\x01\x02\x03\x04")]) def test_store_supports_listing(self, store: ObjectStore) -> None: assert store.supports_listing @@ -64,6 +68,7 @@ def test_store_equal(self, store: ObjectStore) -> None: new_memory_store = ObjectStore(MemoryStore()) assert store != new_memory_store # Test equality against a read only store + assert isinstance(store.store, LocalStore) new_local_store = ObjectStore(LocalStore(prefix=store.store.prefix), read_only=True) assert store != new_local_store # Test two memory stores cannot be equal @@ -73,7 +78,7 @@ def test_store_equal(self, store: ObjectStore) -> None: def test_store_init_raises(self) -> None: """Test __init__ raises appropriate error for improper store type""" with pytest.raises(TypeError): - ObjectStore("path/to/store") + ObjectStore("path/to/store") # type: ignore[arg-type] async def test_store_getsize(self, store: ObjectStore) -> None: buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") @@ -92,10 +97,10 @@ async def test_store_getsize_prefix(self, store: ObjectStore) -> None: @pytest.mark.slow_hypothesis -def test_zarr_hierarchy(): +def test_zarr_hierarchy() -> None: sync_store = ObjectStore(MemoryStore()) def mk_test_instance_sync() -> ZarrHierarchyStateMachine: return ZarrHierarchyStateMachine(sync_store) - run_state_machine_as_test(mk_test_instance_sync) + run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] From 9c5314ca805b130e5a645238da74f1ab702cb7b6 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:28:10 +0100 Subject: [PATCH 04/10] Fix typing in test_stateful --- pyproject.toml | 2 +- tests/test_store/test_stateful.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1b2e6635b5..ac372d6401 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -356,6 +356,7 @@ module = [ "tests.test_store.test_core", "tests.test_store.test_logging", "tests.test_store.test_object", + "tests.test_store.test_stateful", ] strict = false @@ -363,7 +364,6 @@ strict = false # and fix the errors [[tool.mypy.overrides]] module = [ - "tests.test_store.test_stateful", "tests.test_store.test_wrapper", "tests.test_group", "tests.test_indexing", diff --git a/tests/test_store/test_stateful.py b/tests/test_store/test_stateful.py index c0997c3df3..6ea89d91d6 100644 --- a/tests/test_store/test_stateful.py +++ b/tests/test_store/test_stateful.py @@ -16,18 +16,18 @@ @pytest.mark.filterwarnings("ignore::zarr.core.dtype.common.UnstableSpecificationWarning") -def test_zarr_hierarchy(sync_store: Store): +def test_zarr_hierarchy(sync_store: Store) -> None: def mk_test_instance_sync() -> ZarrHierarchyStateMachine: return ZarrHierarchyStateMachine(sync_store) if isinstance(sync_store, ZipStore): pytest.skip(reason="ZipStore does not support delete") - run_state_machine_as_test(mk_test_instance_sync) + run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] def test_zarr_store(sync_store: Store) -> None: - def mk_test_instance_sync() -> None: + def mk_test_instance_sync() -> ZarrStoreStateMachine: return ZarrStoreStateMachine(sync_store) if isinstance(sync_store, ZipStore): @@ -38,4 +38,4 @@ def mk_test_instance_sync() -> None: # It assumes that `set` and `delete` are the only two operations that modify state. # But LocalStore, directories can hang around even after a key is delete-d. pytest.skip(reason="Test isn't suitable for LocalStore.") - run_state_machine_as_test(mk_test_instance_sync) + run_state_machine_as_test(mk_test_instance_sync) # type: ignore[no-untyped-call] From 9dce4d1213ef2464ba1e546353d6e4db79b2ad2f Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:40:23 +0100 Subject: [PATCH 05/10] Fix typing in test_wrapper --- pyproject.toml | 2 +- src/zarr/storage/_wrapper.py | 3 +- tests/test_store/test_wrapper.py | 62 +++++++++++++++++++------------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ac372d6401..ef2ec43444 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -357,6 +357,7 @@ module = [ "tests.test_store.test_logging", "tests.test_store.test_object", "tests.test_store.test_stateful", + "tests.test_store.test_wrapper", ] strict = false @@ -364,7 +365,6 @@ strict = false # and fix the errors [[tool.mypy.overrides]] module = [ - "tests.test_store.test_wrapper", "tests.test_group", "tests.test_indexing", "tests.test_properties", diff --git a/src/zarr/storage/_wrapper.py b/src/zarr/storage/_wrapper.py index f21d378191..23963c8cd6 100644 --- a/src/zarr/storage/_wrapper.py +++ b/src/zarr/storage/_wrapper.py @@ -7,8 +7,9 @@ from types import TracebackType from typing import Any, Self + from zarr.abc.buffer import Buffer from zarr.abc.store import ByteRequest - from zarr.core.buffer import Buffer, BufferPrototype + from zarr.core.buffer import BufferPrototype from zarr.core.common import BytesLike from zarr.abc.store import Store diff --git a/tests/test_store/test_wrapper.py b/tests/test_store/test_wrapper.py index c6edd4f4dd..b0f5bcd9a7 100644 --- a/tests/test_store/test_wrapper.py +++ b/tests/test_store/test_wrapper.py @@ -1,72 +1,82 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypedDict import pytest -from zarr.core.buffer.cpu import Buffer, buffer_prototype +from zarr.abc.store import ByteRequest, Store +from zarr.core.buffer import Buffer +from zarr.core.buffer.cpu import buffer_prototype from zarr.storage import LocalStore, WrapperStore from zarr.testing.store import StoreTests if TYPE_CHECKING: - from _pytest.compat import LEGACY_PATH + from pathlib import Path - from zarr.abc.store import Store from zarr.core.buffer.core import BufferPrototype +class StoreKwargs(TypedDict): + store: LocalStore + + +class OpenKwargs(TypedDict): + store_cls: type[LocalStore] + root: str + + # TODO: fix this warning @pytest.mark.filterwarnings( "ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited:RuntimeWarning" ) -class TestWrapperStore(StoreTests[WrapperStore, Buffer]): - store_cls = WrapperStore +class TestWrapperStore(StoreTests[WrapperStore[LocalStore], Buffer]): + store_cls = WrapperStore[LocalStore] buffer_cls = Buffer - async def get(self, store: WrapperStore, key: str) -> Buffer: + async def get(self, store: WrapperStore[LocalStore], key: str) -> Buffer: return self.buffer_cls.from_bytes((store._store.root / key).read_bytes()) - async def set(self, store: WrapperStore, key: str, value: Buffer) -> None: + async def set(self, store: WrapperStore[LocalStore], key: str, value: Buffer) -> None: parent = (store._store.root / key).parent if not parent.exists(): parent.mkdir(parents=True) (store._store.root / key).write_bytes(value.to_bytes()) @pytest.fixture - def store_kwargs(self, tmpdir: LEGACY_PATH) -> dict[str, str]: - return {"store": LocalStore(str(tmpdir))} + def store_kwargs(self, tmp_path: Path) -> StoreKwargs: + return {"store": LocalStore(str(tmp_path))} @pytest.fixture - def open_kwargs(self, tmpdir) -> dict[str, str]: - return {"store_cls": LocalStore, "root": str(tmpdir)} + def open_kwargs(self, tmp_path: Path) -> OpenKwargs: + return {"store_cls": LocalStore, "root": str(tmp_path)} - def test_store_supports_writes(self, store: WrapperStore) -> None: + def test_store_supports_writes(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_writes - def test_store_supports_partial_writes(self, store: WrapperStore) -> None: + def test_store_supports_partial_writes(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_partial_writes - def test_store_supports_listing(self, store: WrapperStore) -> None: + def test_store_supports_listing(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_listing - def test_store_repr(self, store: WrapperStore) -> None: + def test_store_repr(self, store: WrapperStore[LocalStore]) -> None: assert f"{store!r}" == f"WrapperStore(LocalStore, 'file://{store._store.root.as_posix()}')" - def test_store_str(self, store: WrapperStore) -> None: + def test_store_str(self, store: WrapperStore[LocalStore]) -> None: assert str(store) == f"wrapping-file://{store._store.root.as_posix()}" - def test_check_writeable(self, store: WrapperStore) -> None: + def test_check_writeable(self, store: WrapperStore[LocalStore]) -> None: """ Test _check_writeable() runs without errors. """ store._check_writable() - def test_close(self, store: WrapperStore) -> None: + def test_close(self, store: WrapperStore[LocalStore]) -> None: "Test store can be closed" store.close() assert not store._is_open - def test_is_open_setter_raises(self, store: WrapperStore) -> None: + def test_is_open_setter_raises(self, store: WrapperStore[LocalStore]) -> None: """ Test that a user cannot change `_is_open` without opening the underlying store. """ @@ -83,7 +93,7 @@ def test_is_open_setter_raises(self, store: WrapperStore) -> None: @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=True) async def test_wrapped_set(store: Store, capsys: pytest.CaptureFixture[str]) -> None: # define a class that prints when it sets - class NoisySetter(WrapperStore): + class NoisySetter(WrapperStore[Store]): async def set(self, key: str, value: Buffer) -> None: print(f"setting {key}") await super().set(key, value) @@ -101,15 +111,17 @@ async def set(self, key: str, value: Buffer) -> None: @pytest.mark.parametrize("store", ["local", "memory", "zip"], indirect=True) async def test_wrapped_get(store: Store, capsys: pytest.CaptureFixture[str]) -> None: # define a class that prints when it sets - class NoisyGetter(WrapperStore): - def get(self, key: str, prototype: BufferPrototype) -> None: + class NoisyGetter(WrapperStore[Any]): + async def get( + self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None + ) -> None: print(f"getting {key}") - return super().get(key, prototype=prototype) + await super().get(key, prototype=prototype, byte_range=byte_range) key = "foo" value = Buffer.from_bytes(b"bar") store_wrapped = NoisyGetter(store) await store_wrapped.set(key, value) - assert await store_wrapped.get(key, buffer_prototype) == value + await store_wrapped.get(key, buffer_prototype) captured = capsys.readouterr() assert f"getting {key}" in captured.out From ec646b9fbf940ad447d6554984ee419b41860eea Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 20 Aug 2025 17:54:36 +0100 Subject: [PATCH 06/10] Fix wrapper store tests --- tests/test_store/test_wrapper.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_store/test_wrapper.py b/tests/test_store/test_wrapper.py index b0f5bcd9a7..504f4efa33 100644 --- a/tests/test_store/test_wrapper.py +++ b/tests/test_store/test_wrapper.py @@ -6,6 +6,7 @@ from zarr.abc.store import ByteRequest, Store from zarr.core.buffer import Buffer +from zarr.core.buffer.cpu import Buffer as CPUBuffer from zarr.core.buffer.cpu import buffer_prototype from zarr.storage import LocalStore, WrapperStore from zarr.testing.store import StoreTests @@ -29,9 +30,9 @@ class OpenKwargs(TypedDict): @pytest.mark.filterwarnings( "ignore:coroutine 'ClientCreatorContext.__aexit__' was never awaited:RuntimeWarning" ) -class TestWrapperStore(StoreTests[WrapperStore[LocalStore], Buffer]): - store_cls = WrapperStore[LocalStore] - buffer_cls = Buffer +class TestWrapperStore(StoreTests[WrapperStore[Any], Buffer]): + store_cls = WrapperStore + buffer_cls = CPUBuffer async def get(self, store: WrapperStore[LocalStore], key: str) -> Buffer: return self.buffer_cls.from_bytes((store._store.root / key).read_bytes()) @@ -99,7 +100,7 @@ async def set(self, key: str, value: Buffer) -> None: await super().set(key, value) key = "foo" - value = Buffer.from_bytes(b"bar") + value = CPUBuffer.from_bytes(b"bar") store_wrapped = NoisySetter(store) await store_wrapped.set(key, value) captured = capsys.readouterr() @@ -119,7 +120,7 @@ async def get( await super().get(key, prototype=prototype, byte_range=byte_range) key = "foo" - value = Buffer.from_bytes(b"bar") + value = CPUBuffer.from_bytes(b"bar") store_wrapped = NoisyGetter(store) await store_wrapped.set(key, value) await store_wrapped.get(key, buffer_prototype) From 99835d6ea4471930aea043a9ae3c9a28b140f1b7 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Sun, 24 Aug 2025 10:30:00 +0100 Subject: [PATCH 07/10] Fix store in test --- tests/test_store/test_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index 2dfdaa55de..6589c68e09 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -287,7 +287,7 @@ def test_different_open_mode(tmp_path: LEGACY_PATH) -> None: # Test with a store that doesn't implement .with_read_only() zarr_path = tmp_path / "foo.zarr" zip_store = ZipStore(zarr_path, mode="w") - zarr.create((100,), store=store, zarr_format=2, path="a") + zarr.create((100,), store=zip_store, zarr_format=2, path="a") with pytest.raises( ValueError, 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().", From 2e9ba8f2ac7e8ed89204c5a411c7bc03862d6bfa Mon Sep 17 00:00:00 2001 From: David Stansby Date: Wed, 3 Sep 2025 19:40:07 +0100 Subject: [PATCH 08/10] Fix merge conflicts --- src/zarr/storage/_wrapper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/zarr/storage/_wrapper.py b/src/zarr/storage/_wrapper.py index ccbe0af1dd..64a5b2d83c 100644 --- a/src/zarr/storage/_wrapper.py +++ b/src/zarr/storage/_wrapper.py @@ -10,8 +10,6 @@ from zarr.abc.buffer import Buffer from zarr.abc.store import ByteRequest from zarr.core.buffer import BufferPrototype - from zarr.core.common import BytesLike - from zarr.core.buffer import Buffer, BufferPrototype from zarr.abc.store import Store From ffde28d11aad5e97bdf123a23d666016b8aa0093 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 2 Oct 2025 15:16:57 +0100 Subject: [PATCH 09/10] Adapt ObjectStore tests for new generic class --- tests/test_store/test_object.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 9f5f3aef93..383e9752f1 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -22,8 +22,8 @@ class StoreKwargs(TypedDict): read_only: bool -class TestObjectStore(StoreTests[ObjectStore, cpu.Buffer]): - store_cls = ObjectStore +class TestObjectStore(StoreTests[ObjectStore[LocalStore], cpu.Buffer]): + store_cls = ObjectStore[LocalStore] buffer_cls = cpu.Buffer @pytest.fixture @@ -32,35 +32,35 @@ def store_kwargs(self, tmp_path: Path) -> StoreKwargs: return {"store": store, "read_only": False} @pytest.fixture - def store(self, store_kwargs: StoreKwargs) -> ObjectStore: + def store(self, store_kwargs: StoreKwargs) -> ObjectStore[LocalStore]: return self.store_cls(**store_kwargs) - async def get(self, store: ObjectStore, key: str) -> Buffer: + async def get(self, store: ObjectStore[LocalStore], key: str) -> Buffer: assert isinstance(store.store, LocalStore) new_local_store = LocalStore(prefix=store.store.prefix) return self.buffer_cls.from_bytes(obstore.get(new_local_store, key).bytes()) - async def set(self, store: ObjectStore, key: str, value: Buffer) -> None: + async def set(self, store: ObjectStore[LocalStore], key: str, value: Buffer) -> None: assert isinstance(store.store, LocalStore) new_local_store = LocalStore(prefix=store.store.prefix) obstore.put(new_local_store, key, value.to_bytes()) - def test_store_repr(self, store: ObjectStore) -> None: + def test_store_repr(self, store: ObjectStore[LocalStore]) -> None: from fnmatch import fnmatch pattern = "ObjectStore(object_store://LocalStore(*))" assert fnmatch(f"{store!r}", pattern) - def test_store_supports_writes(self, store: ObjectStore) -> None: + def test_store_supports_writes(self, store: ObjectStore[LocalStore]) -> None: assert store.supports_writes - def test_store_supports_partial_writes(self, store: ObjectStore) -> None: + def test_store_supports_partial_writes(self, store: ObjectStore[LocalStore]) -> None: assert not store.supports_partial_writes - def test_store_supports_listing(self, store: ObjectStore) -> None: + def test_store_supports_listing(self, store: ObjectStore[LocalStore]) -> None: assert store.supports_listing - def test_store_equal(self, store: ObjectStore) -> None: + def test_store_equal(self, store: ObjectStore[LocalStore]) -> None: """Test store equality""" # Test equality against a different instance type assert store != 0 @@ -78,15 +78,15 @@ def test_store_equal(self, store: ObjectStore) -> None: def test_store_init_raises(self) -> None: """Test __init__ raises appropriate error for improper store type""" with pytest.raises(TypeError): - ObjectStore("path/to/store") # type: ignore[arg-type] + ObjectStore("path/to/store") # type: ignore[type-var] - async def test_store_getsize(self, store: ObjectStore) -> None: + async def test_store_getsize(self, store: ObjectStore[LocalStore]) -> None: buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") await self.set(store, "key", buf) size = await store.getsize("key") assert size == len(buf) - async def test_store_getsize_prefix(self, store: ObjectStore) -> None: + async def test_store_getsize_prefix(self, store: ObjectStore[LocalStore]) -> None: buf = cpu.Buffer.from_bytes(b"\x01\x02\x03\x04") await self.set(store, "c/key1/0", buf) await self.set(store, "c/key2/0", buf) From add02bab9bf22216adcac3dbd3d6c7bd5fe59033 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Thu, 2 Oct 2025 15:23:10 +0100 Subject: [PATCH 10/10] Fix generic assignment in tests --- tests/test_store/test_logging.py | 6 ++---- tests/test_store/test_object.py | 3 ++- tests/test_store/test_wrapper.py | 3 --- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_store/test_logging.py b/tests/test_store/test_logging.py index cc6f72331e..e4a0f64b48 100644 --- a/tests/test_store/test_logging.py +++ b/tests/test_store/test_logging.py @@ -22,7 +22,8 @@ class StoreKwargs(TypedDict): class TestLoggingStore(StoreTests[LoggingStore[LocalStore], cpu.Buffer]): - store_cls = LoggingStore[LocalStore] + # store_cls is needed to do an isintsance check, so can't be a subscripted generic + store_cls = LoggingStore # type: ignore[assignment] buffer_cls = cpu.Buffer async def get(self, store: LoggingStore[LocalStore], key: str) -> Buffer: @@ -49,9 +50,6 @@ def store(self, store_kwargs: StoreKwargs) -> LoggingStore[LocalStore]: def test_store_supports_writes(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_writes - def test_store_supports_partial_writes(self, store: LoggingStore[LocalStore]) -> None: - assert store.supports_partial_writes - def test_store_supports_listing(self, store: LoggingStore[LocalStore]) -> None: assert store.supports_listing diff --git a/tests/test_store/test_object.py b/tests/test_store/test_object.py index 383e9752f1..cc0b44f540 100644 --- a/tests/test_store/test_object.py +++ b/tests/test_store/test_object.py @@ -23,7 +23,8 @@ class StoreKwargs(TypedDict): class TestObjectStore(StoreTests[ObjectStore[LocalStore], cpu.Buffer]): - store_cls = ObjectStore[LocalStore] + # store_cls is needed to do an isintsance check, so can't be a subscripted generic + store_cls = ObjectStore # type: ignore[assignment] buffer_cls = cpu.Buffer @pytest.fixture diff --git a/tests/test_store/test_wrapper.py b/tests/test_store/test_wrapper.py index 504f4efa33..b34a63d5d0 100644 --- a/tests/test_store/test_wrapper.py +++ b/tests/test_store/test_wrapper.py @@ -54,9 +54,6 @@ def open_kwargs(self, tmp_path: Path) -> OpenKwargs: def test_store_supports_writes(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_writes - def test_store_supports_partial_writes(self, store: WrapperStore[LocalStore]) -> None: - assert store.supports_partial_writes - def test_store_supports_listing(self, store: WrapperStore[LocalStore]) -> None: assert store.supports_listing