Skip to content

Commit 5e0ffe8

Browse files
committed
Added Store.getsize
Closes #2420
1 parent 329612e commit 5e0ffe8

File tree

4 files changed

+57
-1
lines changed

4 files changed

+57
-1
lines changed

src/zarr/abc/store.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from itertools import starmap
66
from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
77

8+
from zarr.core.buffer.core import default_buffer_prototype
9+
810
if TYPE_CHECKING:
911
from collections.abc import AsyncGenerator, Iterable
1012
from types import TracebackType
@@ -386,6 +388,32 @@ async def _get_many(
386388
for req in requests:
387389
yield (req[0], await self.get(*req))
388390

391+
async def getsize(self, key: str) -> int:
392+
"""
393+
Return the size, in bytes, of a value in a Store.
394+
395+
Parameters
396+
----------
397+
key : str
398+
399+
Returns
400+
-------
401+
nbytes: int
402+
The size of the value in bytes.
403+
404+
Raises
405+
------
406+
FileNotFoundError
407+
When the given key does not exist in the store.
408+
"""
409+
# Note to implementers: this default implementation is very inefficient since
410+
# it requires reading the entire object. Many systems will have ways to get the
411+
# size of an object without reading it.
412+
value = await self.get(key, prototype=default_buffer_prototype())
413+
if value is None:
414+
raise FileNotFoundError(key)
415+
return len(value)
416+
389417

390418
@runtime_checkable
391419
class ByteGetter(Protocol):

src/zarr/storage/local.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,6 @@ async def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
242242
yield str(key).replace(to_strip, "")
243243
except (FileNotFoundError, NotADirectoryError):
244244
pass
245+
246+
async def getsize(self, key: str) -> int:
247+
return os.path.getsize(self.root / key)

src/zarr/storage/remote.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING, Any, Self
3+
from typing import TYPE_CHECKING, Any, Self, cast
44

55
import fsspec
66

@@ -301,3 +301,16 @@ async def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]:
301301
find_str = f"{self.path}/{prefix}"
302302
for onefile in await self.fs._find(find_str, detail=False, maxdepth=None, withdirs=False):
303303
yield onefile.removeprefix(find_str)
304+
305+
async def getsize(self, key: str) -> int:
306+
path = _dereference_path(self.path, key)
307+
info = await self.fs._info(path)
308+
309+
size = info.get("size")
310+
311+
if size is None:
312+
# Not all filesystems support size. Fall back to reading the entire object
313+
return await super().getsize(key)
314+
else:
315+
# fsspec doesn't have typing. We'll need to assume this is correct.
316+
return cast(int, size)

src/zarr/testing/store.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,15 @@ async def test_set_if_not_exists(self, store: S) -> None:
338338

339339
result = await store.get("k2", default_buffer_prototype())
340340
assert result == new
341+
342+
async def test_getsize(self, store: S) -> None:
343+
key = "k"
344+
data = self.buffer_cls.from_bytes(b"0" * 10)
345+
await self.set(store, key, data)
346+
347+
result = await store.getsize(key)
348+
assert result == 10
349+
350+
async def test_getsize_raises(self, store: S) -> None:
351+
with pytest.raises(FileNotFoundError):
352+
await store.getsize("not-a-real-key")

0 commit comments

Comments
 (0)