Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve test coverage for storage classes #2693

Merged
merged 50 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
2ea442c
Run Store tests on logging
maxrjones Jan 11, 2025
7f76575
Run store tests on wrapper
maxrjones Jan 11, 2025
98b7392
Add read only open tests to WrapperStore
maxrjones Jan 11, 2025
18be47f
Ignore new coverage files
maxrjones Jan 11, 2025
69ce1d7
Simplify wrapper tests
maxrjones Jan 11, 2025
5877355
Fix __eq__ method in WrapperStore
maxrjones Jan 11, 2025
b4310fd
Implement __repr__ for WrapperStore
maxrjones Jan 11, 2025
d08458e
Allow separate open and init kwargs
maxrjones Jan 11, 2025
f663694
Add open class method to LoggingStore
maxrjones Jan 11, 2025
cf62f67
Add __str__ to WrapperStore
maxrjones Jan 11, 2025
31f9931
Add repr test for LoggingStore
maxrjones Jan 11, 2025
964aeaa
Fix __eq__ in LoggingStore
maxrjones Jan 11, 2025
332f564
Test getsize for stores
maxrjones Jan 11, 2025
4d4d728
Test for invalid ByteRequest
maxrjones Jan 11, 2025
30d1323
Use stdout rather than stderr as the default logging stream
maxrjones Jan 12, 2025
9764204
Test default logging stream
maxrjones Jan 12, 2025
fefd666
Add test for getsize_prefix
maxrjones Jan 12, 2025
6f240c2
Document buffer prototype parameter
maxrjones Jan 12, 2025
d2bbd9d
Add test for invalid modes in StorePath.open()
maxrjones Jan 12, 2025
85f44db
Add test for contains_group
maxrjones Jan 12, 2025
51c0c15
Add tests for contains_array
maxrjones Jan 12, 2025
ddd6bc9
Test for invalid root type for LocalStore
maxrjones Jan 12, 2025
62a528c
Test LocalStore.get with default prototype
maxrjones Jan 12, 2025
5f00efd
Test for invalid set buffer arguments
maxrjones Jan 13, 2025
6923337
Test get and set on closed stores
maxrjones Jan 13, 2025
0792fa8
Test using stores in a context manager
maxrjones Jan 13, 2025
dd0de05
Specify abstract methods for StoreTests
maxrjones Jan 13, 2025
4dba40c
Apply suggestions from code review
maxrjones Jan 13, 2025
48abe94
Lint
maxrjones Jan 14, 2025
bf4589d
Fix typing for LoggingStore
maxrjones Jan 14, 2025
5b37417
Match specific Errors in tests
maxrjones Jan 14, 2025
74647de
Add docstring
maxrjones Jan 14, 2025
c8ebcd0
Parametrize tests
maxrjones Jan 14, 2025
1e96600
Test for contains group/array at multiple heirarchies
maxrjones Jan 14, 2025
cc14e07
Update TypeError on GpuMemoryStore
maxrjones Jan 14, 2025
5148dd6
Merge branch 'main' into testing-storage
maxrjones Jan 15, 2025
bf58808
Don't implement _is_open setter on wrapped stores
maxrjones Jan 16, 2025
e1caef0
Update reprs for LoggingStore and WrapperStore
maxrjones Jan 16, 2025
1922d2d
Test check_writeable and close for WrapperStore
maxrjones Jan 16, 2025
45ea40d
Update pull request template (#2717)
brokkoli71 Jan 16, 2025
b281bc9
Add release notes
maxrjones Jan 16, 2025
9a75da2
Merge branch 'main' into testing-storage
maxrjones Jan 16, 2025
ca83bd6
Merge branch 'main' into testing-storage
maxrjones Jan 23, 2025
0d6eccf
Comprehensive changelog entry
maxrjones Jan 23, 2025
9059135
Match error message
maxrjones Jan 23, 2025
1e080c0
Merge branch 'main' into testing-storage
maxrjones Jan 24, 2025
1587fd1
Apply suggestions from code review
maxrjones Jan 24, 2025
5e16b3d
Update 2693.bugfix.rst
maxrjones Jan 24, 2025
128ef99
Merge branch 'main' into testing-storage
jhamman Jan 25, 2025
1faa66c
Merge branch 'main' into testing-storage
d-v-b Jan 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ src/zarr/_version.py
data/*
src/fixture/
fixture/
junit.xml

.DS_Store
tests/.hypothesis
Expand Down
13 changes: 13 additions & 0 deletions changes/2693.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Implement open() for LoggingStore
LoggingStore is now a generic class.
Use stdout rather than stderr as the default stream for LoggingStore
Ensure that ZipStore is open before getting or setting any values
Update equality for LoggingStore and WrapperStore such that the types must be equal.
Indicate StoreTest's `test_store_repr`, `test_store_supports_writes`, `test_store_supports_partial_writes`, and `test_store_supports_listing` need to be implemented using `@abstractmethod` rather than `NotImplementedError`.
Separate instantiating and opening a store in StoreTests
Test using Store as a context manager in StoreTests
Match the errors raised by read only stores in StoreTests
Test that a ValueError is raise for invalid byte range syntax in StoreTests
Test getsize() and getsize_prefix() in StoreTests
Test the error raised for invalid buffer arguments in StoreTests
Test that data can be written to a store that's not yet open using the store.set method in StoreTests
6 changes: 4 additions & 2 deletions src/zarr/abc/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,10 @@ async def get(
Parameters
----------
key : str
prototype : BufferPrototype
The prototype of the output buffer. Stores may support a default buffer prototype.
byte_range : ByteRequest, optional

ByteRequest may be one of the following. If not provided, all data associated with the key is retrieved.

- RangeByteRequest(int, int): Request a specific range of bytes in the form (start, end). The end is exclusive. If the given range is zero-length or starts after the end of the object, an error will be returned. Additionally, if the range ends after the end of the object, the entire remainder of the object will be returned. Otherwise, the exact requested range will be returned.
- OffsetByteRequest(int): Request all bytes starting from a given byte offset. This is equivalent to bytes={int}- as an HTTP header.
- SuffixByteRequest(int): Request the last int bytes. Note that here, int is the size of the request, not the byte offset. This is equivalent to bytes=-{int} as an HTTP header.
Expand All @@ -200,6 +200,8 @@ async def get_partial_values(

Parameters
----------
prototype : BufferPrototype
The prototype of the output buffer. Stores may support a default buffer prototype.
key_ranges : Iterable[tuple[str, tuple[int | None, int | None]]]
Ordered set of key, range pairs, a key may occur multiple times with different ranges

Expand Down
7 changes: 6 additions & 1 deletion src/zarr/storage/_fsspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
Store,
SuffixByteRequest,
)
from zarr.core.buffer import Buffer
from zarr.storage._common import _dereference_path

if TYPE_CHECKING:
from collections.abc import AsyncIterator, Iterable

from fsspec.asyn import AsyncFileSystem

from zarr.core.buffer import Buffer, BufferPrototype
from zarr.core.buffer import BufferPrototype
from zarr.core.common import BytesLike


Expand Down Expand Up @@ -264,6 +265,10 @@ async def set(
if not self._is_open:
await self._open()
self._check_writable()
if not isinstance(value, Buffer):
raise TypeError(
f"FsspecStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
)
path = _dereference_path(self.path, key)
# write data
if byte_range:
Expand Down
6 changes: 4 additions & 2 deletions src/zarr/storage/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def __init__(self, root: Path | str, *, read_only: bool = False) -> None:
root = Path(root)
if not isinstance(root, Path):
raise TypeError(
f'"root" must be a string or Path instance. Got an object with type {type(root)} instead.'
f"'root' must be a string or Path instance. Got an instance of {type(root)} instead."
)
self.root = root

Expand Down Expand Up @@ -169,7 +169,9 @@ async def _set(self, key: str, value: Buffer, exclusive: bool = False) -> None:
self._check_writable()
assert isinstance(key, str)
if not isinstance(value, Buffer):
raise TypeError("LocalStore.set(): `value` must a Buffer instance")
raise TypeError(
f"LocalStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
)
path = self.root / key
await asyncio.to_thread(_put, path, value, start=None, exclusive=exclusive)

Expand Down
26 changes: 18 additions & 8 deletions src/zarr/storage/_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import inspect
import logging
import sys
import time
from collections import defaultdict
from contextlib import contextmanager
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Self, TypeVar

from zarr.abc.store import Store
from zarr.storage._wrapper import WrapperStore
Expand All @@ -18,8 +19,10 @@

counter: defaultdict[str, int]

T_Store = TypeVar("T_Store", bound=Store)

class LoggingStore(WrapperStore[Store]):

class LoggingStore(WrapperStore[T_Store]):
"""
Store wrapper that logs all calls to the wrapped store.

Expand All @@ -42,7 +45,7 @@ class LoggingStore(WrapperStore[Store]):

def __init__(
self,
store: Store,
store: T_Store,
log_level: str = "DEBUG",
log_handler: logging.Handler | None = None,
) -> None:
Expand All @@ -67,7 +70,7 @@ def _configure_logger(

def _default_handler(self) -> logging.Handler:
"""Define a default log handler"""
handler = logging.StreamHandler()
handler = logging.StreamHandler(stream=sys.stdout)
handler.setLevel(self.log_level)
handler.setFormatter(
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
Expand All @@ -94,6 +97,14 @@ def log(self, hint: Any = "") -> Generator[None, None, None]:
end_time = time.time()
self.logger.info("Finished %s [%.2f s]", op, end_time - start_time)

@classmethod
async def open(cls: type[Self], store_cls: type[T_Store], *args: Any, **kwargs: Any) -> Self:
log_level = kwargs.pop("log_level", "DEBUG")
log_handler = kwargs.pop("log_handler", None)
store = store_cls(*args, **kwargs)
await store._open()
return cls(store=store, log_level=log_level, log_handler=log_handler)

@property
def supports_writes(self) -> bool:
with self.log():
Expand Down Expand Up @@ -126,8 +137,7 @@ def _is_open(self) -> bool:

@_is_open.setter
def _is_open(self, value: bool) -> None:
with self.log(value):
self._store._is_open = value
raise NotImplementedError("LoggingStore must be opened via the `_open` method")

async def _open(self) -> None:
with self.log():
Expand All @@ -151,11 +161,11 @@ def __str__(self) -> str:
return f"logging-{self._store}"

def __repr__(self) -> str:
return f"LoggingStore({repr(self._store)!r})"
return f"LoggingStore({self._store.__class__.__name__}, '{self._store}')"

def __eq__(self, other: object) -> bool:
with self.log(other):
return self._store == other
return type(self) is type(other) and self._store.__eq__(other._store) # type: ignore[attr-defined]

async def get(
self,
Expand Down
9 changes: 6 additions & 3 deletions src/zarr/storage/_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,9 @@
await self._ensure_open()
assert isinstance(key, str)
if not isinstance(value, Buffer):
raise TypeError(f"Expected Buffer. Got {type(value)}.")
raise TypeError(
f"MemoryStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
)

if byte_range is not None:
buf = self._store_dict[key]
Expand Down Expand Up @@ -231,8 +233,9 @@
self._check_writable()
assert isinstance(key, str)
if not isinstance(value, Buffer):
raise TypeError(f"Expected Buffer. Got {type(value)}.")

raise TypeError(

Check warning on line 236 in src/zarr/storage/_memory.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_memory.py#L236

Added line #L236 was not covered by tests
f"GpuMemoryStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
)
# Convert to gpu.Buffer
gpu_value = value if isinstance(value, gpu.Buffer) else gpu.Buffer.from_buffer(value)
await super().set(key, gpu_value, byte_range=byte_range)
16 changes: 15 additions & 1 deletion src/zarr/storage/_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ async def _ensure_open(self) -> None:
async def is_empty(self, prefix: str) -> bool:
return await self._store.is_empty(prefix)

@property
def _is_open(self) -> bool:
return self._store._is_open

@_is_open.setter
def _is_open(self, value: bool) -> None:
raise NotImplementedError("WrapperStore must be opened via the `_open` method")

async def clear(self) -> None:
return await self._store.clear()

Expand All @@ -67,7 +75,13 @@ def _check_writable(self) -> None:
return self._store._check_writable()

def __eq__(self, value: object) -> bool:
return type(self) is type(value) and self._store.__eq__(value)
return type(self) is type(value) and self._store.__eq__(value._store) # type: ignore[attr-defined]

def __str__(self) -> str:
return f"wrapping-{self._store}"

def __repr__(self) -> str:
return f"WrapperStore({self._store.__class__.__name__}, '{self._store}')"

async def get(
self, key: str, prototype: BufferPrototype, byte_range: ByteRequest | None = None
Expand Down
10 changes: 9 additions & 1 deletion src/zarr/storage/_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@
prototype: BufferPrototype,
byte_range: ByteRequest | None = None,
) -> Buffer | None:
if not self._is_open:
self._sync_open()

Check warning on line 150 in src/zarr/storage/_zip.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_zip.py#L150

Added line #L150 was not covered by tests
# docstring inherited
try:
with self._zf.open(key) as f: # will raise KeyError
Expand Down Expand Up @@ -190,6 +192,8 @@
return out

def _set(self, key: str, value: Buffer) -> None:
if not self._is_open:
self._sync_open()
# generally, this should be called inside a lock
keyinfo = zipfile.ZipInfo(filename=key, date_time=time.localtime(time.time())[:6])
keyinfo.compress_type = self.compression
Expand All @@ -203,9 +207,13 @@
async def set(self, key: str, value: Buffer) -> None:
# docstring inherited
self._check_writable()
if not self._is_open:
self._sync_open()
assert isinstance(key, str)
if not isinstance(value, Buffer):
raise TypeError("ZipStore.set(): `value` must a Buffer instance")
raise TypeError(
f"ZipStore.set(): `value` must be a Buffer instance. Got an instance of {type(value)} instead."
)
with self._lock:
self._set(key, value)

Expand Down
Loading
Loading