Skip to content

Commit fa13860

Browse files
committed
add path to stores
1 parent 9696222 commit fa13860

File tree

13 files changed

+109
-72
lines changed

13 files changed

+109
-72
lines changed

src/zarr/abc/store.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,17 @@ class Store(ABC):
5151
# 'attributes')
5252
# raise NotImplementedError(msg)
5353

54-
def __init__(
55-
self, path: str = "", mode: AccessModeLiteral = "r", *args: Any, **kwargs: Any
56-
) -> None:
54+
def __init__(self, path: str = "", mode: AccessModeLiteral = "r") -> None:
5755
object.__setattr__(self, "_is_open", False)
5856
object.__setattr__(self, "_mode", AccessMode.from_literal(mode))
59-
object.__setattr__(self, "path", path)
57+
object.__setattr__(self, "path", validate_path(path))
58+
59+
def resolve_key(self, key: str) -> str:
60+
key = parse_path(key)
61+
if self.path == "":
62+
return key
63+
else:
64+
return f"{self.path}/{key}"
6065

6166
@classmethod
6267
async def open(cls, *args: Any, **kwargs: Any) -> Self:
@@ -335,8 +340,8 @@ def with_path(self, path: str) -> Self:
335340
"""
336341
Return a copy of this store with a new path attribute
337342
"""
338-
# TODO: implement this
339-
return self
343+
# TODO: implement me
344+
raise NotImplementedError
340345

341346

342347
@runtime_checkable
@@ -364,3 +369,20 @@ async def set_or_delete(byte_setter: ByteSetter, value: Buffer | None) -> None:
364369
await byte_setter.delete()
365370
else:
366371
await byte_setter.set(value)
372+
373+
374+
def validate_path(path: str) -> str:
375+
"""
376+
Ensure that the input string is a valid relative path in the abstract zarr object storage scheme.
377+
"""
378+
if path.endswith("/"):
379+
raise ValueError(f"Invalid path: {path} ends with '/'.")
380+
if "//" in path:
381+
raise ValueError(f"Invalid path: {path} contains '//'.")
382+
if "\\" in path:
383+
raise ValueError(f"Invalid path: {path} contains '\"'.")
384+
return path
385+
386+
387+
def parse_path(path: str) -> str:
388+
return path.rstrip("/").lstrip("/")

src/zarr/store/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ async def make_store_path(
101101
mode = "w" # exception to the default mode = 'r'
102102
result = StorePath(await MemoryStore.open(mode=mode))
103103
elif isinstance(store_like, Path):
104-
result = StorePath(await LocalStore.open(root=store_like, mode=mode or "r"))
104+
result = StorePath(await LocalStore.open(path=store_like, mode=mode or "r"))
105105
elif isinstance(store_like, str):
106106
storage_options = storage_options or {}
107107

@@ -111,7 +111,7 @@ async def make_store_path(
111111
RemoteStore.from_url(store_like, storage_options=storage_options, mode=mode or "r")
112112
)
113113
else:
114-
result = StorePath(await LocalStore.open(root=Path(store_like), mode=mode or "r"))
114+
result = StorePath(await LocalStore.open(path=store_like, mode=mode or "r"))
115115
elif isinstance(store_like, dict):
116116
# We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings.
117117
# By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate.

src/zarr/store/local.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
import os
55
import shutil
66
from pathlib import Path
7-
from typing import TYPE_CHECKING, Self
7+
from typing import TYPE_CHECKING
88

99
from zarr.abc.store import ByteRangeRequest, Store
1010
from zarr.core.buffer import Buffer
1111
from zarr.core.common import concurrent_map, to_thread
1212

1313
if TYPE_CHECKING:
1414
from collections.abc import AsyncGenerator, Iterable
15+
from typing import Self
1516

1617
from zarr.core.buffer import BufferPrototype
1718
from zarr.core.common import AccessModeLiteral
@@ -125,7 +126,7 @@ async def get(
125126
) -> Buffer | None:
126127
if not self._is_open:
127128
await self._open()
128-
assert isinstance(key, str)
129+
129130
path = os.path.join(self.path, key)
130131

131132
try:

src/zarr/store/logging.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
import time
66
from collections import defaultdict
77
from contextlib import contextmanager
8-
from typing import TYPE_CHECKING, Self
8+
from typing import TYPE_CHECKING
99

1010
from zarr.abc.store import AccessMode, ByteRangeRequest, Store
1111
from zarr.core.buffer import Buffer
1212

1313
if TYPE_CHECKING:
1414
from collections.abc import AsyncGenerator, Generator, Iterable
15+
from typing import Self
1516

1617
from zarr.core.buffer import Buffer, BufferPrototype
1718
from zarr.core.common import AccessModeLiteral

src/zarr/store/memory.py

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

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

55
from zarr.abc.store import ByteRangeRequest, Store
66
from zarr.core.buffer import Buffer, gpu
@@ -9,6 +9,7 @@
99

1010
if TYPE_CHECKING:
1111
from collections.abc import AsyncGenerator, Iterable, MutableMapping
12+
from typing import Self
1213

1314
from zarr.core.buffer import BufferPrototype
1415
from zarr.core.common import AccessModeLiteral
@@ -26,9 +27,9 @@ class MemoryStore(Store):
2627

2728
def __init__(
2829
self,
29-
path: str = "",
3030
store_dict: MutableMapping[str, Buffer] | None = None,
3131
*,
32+
path: str = "",
3233
mode: AccessModeLiteral = "r",
3334
) -> None:
3435
super().__init__(mode=mode, path=path)
@@ -68,9 +69,9 @@ async def get(
6869
) -> Buffer | None:
6970
if not self._is_open:
7071
await self._open()
71-
assert isinstance(key, str)
72+
7273
try:
73-
value = self._store_dict[key]
74+
value = self._store_dict[self.resolve_key(key)]
7475
start, length = _normalize_interval_index(value, byte_range)
7576
return prototype.buffer.from_buffer(value[start : start + length])
7677
except KeyError:
@@ -88,45 +89,46 @@ async def _get(key: str, byte_range: ByteRangeRequest) -> Buffer | None:
8889
return await concurrent_map(key_ranges, _get, limit=None)
8990

9091
async def exists(self, key: str) -> bool:
91-
return key in self._store_dict
92+
return self.resolve_key(key) in self._store_dict
9293

9394
async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None:
9495
self._check_writable()
9596
await self._ensure_open()
9697
assert isinstance(key, str)
9798
if not isinstance(value, Buffer):
9899
raise TypeError(f"Expected Buffer. Got {type(value)}.")
99-
100+
key_abs = self.resolve_key(key)
100101
if byte_range is not None:
101-
buf = self._store_dict[key]
102+
buf = self._store_dict[key_abs]
102103
buf[byte_range[0] : byte_range[1]] = value
103-
self._store_dict[key] = buf
104+
self._store_dict[key_abs] = buf
104105
else:
105-
self._store_dict[key] = value
106+
self._store_dict[key_abs] = value
106107

107108
async def set_if_not_exists(self, key: str, default: Buffer) -> None:
108109
self._check_writable()
109110
await self._ensure_open()
110-
self._store_dict.setdefault(key, default)
111+
self._store_dict.setdefault(self.resolve_key(key), default)
111112

112113
async def delete(self, key: str) -> None:
113114
self._check_writable()
114115
try:
115-
del self._store_dict[key]
116+
del self._store_dict[self.resolve_key(key)]
116117
except KeyError:
117118
pass # Q(JH): why not raise?
118119

119120
async def set_partial_values(self, key_start_values: Iterable[tuple[str, int, bytes]]) -> None:
120121
raise NotImplementedError
121122

122123
async def list(self) -> AsyncGenerator[str, None]:
123-
for key in self._store_dict:
124-
yield key
124+
async for result in self.list_prefix(""):
125+
yield result
125126

126127
async def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]:
128+
prefix_abs = self.resolve_key(prefix)
127129
for key in self._store_dict:
128-
if key.startswith(prefix):
129-
yield key.removeprefix(prefix)
130+
if key.startswith(prefix_abs):
131+
yield key.removeprefix(prefix_abs).lstrip("/")
130132

131133
async def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
132134
"""
@@ -141,8 +143,7 @@ async def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
141143
-------
142144
AsyncGenerator[str, None]
143145
"""
144-
if prefix.endswith("/"):
145-
prefix = prefix[:-1]
146+
prefix = self.resolve_key(prefix)
146147

147148
if prefix == "":
148149
keys_unique = {k.split("/")[0] for k in self._store_dict}

src/zarr/store/remote.py

Lines changed: 2 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
44

55
import fsspec
66

@@ -10,6 +10,7 @@
1010

1111
if TYPE_CHECKING:
1212
from collections.abc import AsyncGenerator, Iterable
13+
from typing import Self
1314

1415
from fsspec.asyn import AsyncFileSystem
1516

0 commit comments

Comments
 (0)