Skip to content

Commit c3cf284

Browse files
committed
Merge branch 'v3' of https://github.com/zarr-developers/zarr-python into hierarchy_api
2 parents 94a60ae + ef15e20 commit c3cf284

File tree

11 files changed

+279
-835
lines changed

11 files changed

+279
-835
lines changed

src/zarr/abc/store.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,34 @@
33
from typing import Protocol, runtime_checkable
44

55
from zarr.buffer import Buffer
6-
from zarr.common import BytesLike
6+
from zarr.common import BytesLike, OpenMode
77

88

99
class Store(ABC):
10+
_mode: OpenMode
11+
12+
def __init__(self, mode: OpenMode = "r"):
13+
if mode not in ("r", "r+", "w", "w-", "a"):
14+
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")
15+
self._mode = mode
16+
17+
@property
18+
def mode(self) -> OpenMode:
19+
"""Access mode of the store."""
20+
return self._mode
21+
22+
@property
23+
def writeable(self) -> bool:
24+
"""Is the store writeable?"""
25+
return self.mode in ("a", "w", "w-")
26+
27+
def _check_writable(self) -> None:
28+
if not self.writeable:
29+
raise ValueError("store mode does not support writing")
30+
1031
@abstractmethod
1132
async def get(
12-
self, key: str, byte_range: tuple[int, int | None] | None = None
33+
self, key: str, byte_range: tuple[int | None, int | None] | None = None
1334
) -> Buffer | None:
1435
"""Retrieve the value associated with a given key.
1536
@@ -26,7 +47,7 @@ async def get(
2647

2748
@abstractmethod
2849
async def get_partial_values(
29-
self, key_ranges: list[tuple[str, tuple[int, int]]]
50+
self, key_ranges: list[tuple[str, tuple[int | None, int | None]]]
3051
) -> list[Buffer | None]:
3152
"""Retrieve possibly partial values from given key_ranges.
3253
@@ -147,6 +168,10 @@ def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
147168
"""
148169
...
149170

171+
def close(self) -> None: # noqa: B027
172+
"""Close the store."""
173+
pass
174+
150175

151176
@runtime_checkable
152177
class ByteGetter(Protocol):

src/zarr/common.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Selection = slice | SliceSelection
2828
ZarrFormat = Literal[2, 3]
2929
JSON = None | str | int | float | Enum | dict[str, "JSON"] | list["JSON"] | tuple["JSON", ...]
30+
OpenMode = Literal["r", "r+", "a", "w", "w-"]
3031

3132

3233
def product(tup: ChunkCoords) -> int:

src/zarr/store/core.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from zarr.abc.store import Store
77
from zarr.buffer import Buffer
8+
from zarr.common import OpenMode
89
from zarr.store.local import LocalStore
910

1011

@@ -60,11 +61,40 @@ def __eq__(self, other: Any) -> bool:
6061
StoreLike = Store | StorePath | Path | str
6162

6263

63-
def make_store_path(store_like: StoreLike) -> StorePath:
64+
def make_store_path(store_like: StoreLike, *, mode: OpenMode | None = None) -> StorePath:
6465
if isinstance(store_like, StorePath):
66+
if mode is not None:
67+
assert mode == store_like.store.mode
6568
return store_like
6669
elif isinstance(store_like, Store):
70+
if mode is not None:
71+
assert mode == store_like.mode
6772
return StorePath(store_like)
6873
elif isinstance(store_like, str):
69-
return StorePath(LocalStore(Path(store_like)))
74+
assert mode is not None
75+
return StorePath(LocalStore(Path(store_like), mode=mode))
7076
raise TypeError
77+
78+
79+
def _normalize_interval_index(
80+
data: Buffer, interval: None | tuple[int | None, int | None]
81+
) -> tuple[int, int]:
82+
"""
83+
Convert an implicit interval into an explicit start and length
84+
"""
85+
if interval is None:
86+
start = 0
87+
length = len(data)
88+
else:
89+
maybe_start, maybe_len = interval
90+
if maybe_start is None:
91+
start = 0
92+
else:
93+
start = maybe_start
94+
95+
if maybe_len is None:
96+
length = len(data) - start
97+
else:
98+
length = maybe_len
99+
100+
return (start, length)

src/zarr/store/local.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@
77

88
from zarr.abc.store import Store
99
from zarr.buffer import Buffer
10-
from zarr.common import concurrent_map, to_thread
10+
from zarr.common import OpenMode, concurrent_map, to_thread
1111

1212

13-
def _get(path: Path, byte_range: tuple[int, int | None] | None) -> Buffer:
13+
def _get(path: Path, byte_range: tuple[int | None, int | None] | None) -> Buffer:
1414
"""
1515
Fetch a contiguous region of bytes from a file.
1616
@@ -51,10 +51,8 @@ def _put(
5151
path: Path,
5252
value: Buffer,
5353
start: int | None = None,
54-
auto_mkdir: bool = True,
5554
) -> int | None:
56-
if auto_mkdir:
57-
path.parent.mkdir(parents=True, exist_ok=True)
55+
path.parent.mkdir(parents=True, exist_ok=True)
5856
if start is not None:
5957
with path.open("r+b") as f:
6058
f.seek(start)
@@ -70,15 +68,14 @@ class LocalStore(Store):
7068
supports_listing: bool = True
7169

7270
root: Path
73-
auto_mkdir: bool
7471

75-
def __init__(self, root: Path | str, auto_mkdir: bool = True):
72+
def __init__(self, root: Path | str, *, mode: OpenMode = "r"):
73+
super().__init__(mode=mode)
7674
if isinstance(root, str):
7775
root = Path(root)
7876
assert isinstance(root, Path)
7977

8078
self.root = root
81-
self.auto_mkdir = auto_mkdir
8279

8380
def __str__(self) -> str:
8481
return f"file://{self.root}"
@@ -90,7 +87,7 @@ def __eq__(self, other: object) -> bool:
9087
return isinstance(other, type(self)) and self.root == other.root
9188

9289
async def get(
93-
self, key: str, byte_range: tuple[int, int | None] | None = None
90+
self, key: str, byte_range: tuple[int | None, int | None] | None = None
9491
) -> Buffer | None:
9592
assert isinstance(key, str)
9693
path = self.root / key
@@ -101,7 +98,7 @@ async def get(
10198
return None
10299

103100
async def get_partial_values(
104-
self, key_ranges: list[tuple[str, tuple[int, int]]]
101+
self, key_ranges: list[tuple[str, tuple[int | None, int | None]]]
105102
) -> list[Buffer | None]:
106103
"""
107104
Read byte ranges from multiple keys.
@@ -121,16 +118,18 @@ async def get_partial_values(
121118
return await concurrent_map(args, to_thread, limit=None) # TODO: fix limit
122119

123120
async def set(self, key: str, value: Buffer) -> None:
121+
self._check_writable()
124122
assert isinstance(key, str)
125123
if isinstance(value, bytes | bytearray):
126124
# TODO: to support the v2 tests, we convert bytes to Buffer here
127125
value = Buffer.from_bytes(value)
128126
if not isinstance(value, Buffer):
129127
raise TypeError("LocalStore.set(): `value` must a Buffer instance")
130128
path = self.root / key
131-
await to_thread(_put, path, value, auto_mkdir=self.auto_mkdir)
129+
await to_thread(_put, path, value)
132130

133131
async def set_partial_values(self, key_start_values: list[tuple[str, int, bytes]]) -> None:
132+
self._check_writable()
134133
args = []
135134
for key, start, value in key_start_values:
136135
assert isinstance(key, str)
@@ -142,6 +141,7 @@ async def set_partial_values(self, key_start_values: list[tuple[str, int, bytes]
142141
await concurrent_map(args, to_thread, limit=None) # TODO: fix limit
143142

144143
async def delete(self, key: str) -> None:
144+
self._check_writable()
145145
path = self.root / key
146146
if path.is_dir(): # TODO: support deleting directories? shutil.rmtree?
147147
shutil.rmtree(path)

src/zarr/store/memory.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from zarr.abc.store import Store
66
from zarr.buffer import Buffer
7-
from zarr.common import concurrent_map
7+
from zarr.common import OpenMode, concurrent_map
8+
from zarr.store.core import _normalize_interval_index
89

910

1011
# TODO: this store could easily be extended to wrap any MutableMapping store from v2
@@ -16,7 +17,10 @@ class MemoryStore(Store):
1617

1718
_store_dict: MutableMapping[str, Buffer]
1819

19-
def __init__(self, store_dict: MutableMapping[str, Buffer] | None = None):
20+
def __init__(
21+
self, store_dict: MutableMapping[str, Buffer] | None = None, *, mode: OpenMode = "r"
22+
):
23+
super().__init__(mode=mode)
2024
self._store_dict = store_dict or {}
2125

2226
def __str__(self) -> str:
@@ -26,19 +30,18 @@ def __repr__(self) -> str:
2630
return f"MemoryStore({str(self)!r})"
2731

2832
async def get(
29-
self, key: str, byte_range: tuple[int, int | None] | None = None
33+
self, key: str, byte_range: tuple[int | None, int | None] | None = None
3034
) -> Buffer | None:
3135
assert isinstance(key, str)
3236
try:
3337
value = self._store_dict[key]
34-
if byte_range is not None:
35-
value = value[byte_range[0] : byte_range[1]]
36-
return value
38+
start, length = _normalize_interval_index(value, byte_range)
39+
return value[start : start + length]
3740
except KeyError:
3841
return None
3942

4043
async def get_partial_values(
41-
self, key_ranges: list[tuple[str, tuple[int, int]]]
44+
self, key_ranges: list[tuple[str, tuple[int | None, int | None]]]
4245
) -> list[Buffer | None]:
4346
vals = await concurrent_map(key_ranges, self.get, limit=None)
4447
return vals
@@ -47,6 +50,7 @@ async def exists(self, key: str) -> bool:
4750
return key in self._store_dict
4851

4952
async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None:
53+
self._check_writable()
5054
assert isinstance(key, str)
5155
if isinstance(value, bytes | bytearray):
5256
# TODO: to support the v2 tests, we convert bytes to Buffer here
@@ -62,6 +66,7 @@ async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None
6266
self._store_dict[key] = value
6367

6468
async def delete(self, key: str) -> None:
69+
self._check_writable()
6570
try:
6671
del self._store_dict[key]
6772
except KeyError:

src/zarr/store/remote.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from zarr.abc.store import Store
66
from zarr.buffer import Buffer
7+
from zarr.common import OpenMode
78
from zarr.store.core import _dereference_path
89

910
if TYPE_CHECKING:
@@ -18,17 +19,22 @@ class RemoteStore(Store):
1819

1920
root: UPath
2021

21-
def __init__(self, url: UPath | str, **storage_options: dict[str, Any]):
22+
def __init__(
23+
self, url: UPath | str, *, mode: OpenMode = "r", **storage_options: dict[str, Any]
24+
):
2225
import fsspec
2326
from upath import UPath
2427

28+
super().__init__(mode=mode)
29+
2530
if isinstance(url, str):
2631
self.root = UPath(url, **storage_options)
2732
else:
2833
assert (
2934
len(storage_options) == 0
3035
), "If constructed with a UPath object, no additional storage_options are allowed."
3136
self.root = url.rstrip("/")
37+
3238
# test instantiate file system
3339
fs, _ = fsspec.core.url_to_fs(str(self.root), asynchronous=True, **self.root._kwargs)
3440
assert fs.__class__.async_impl, "FileSystem needs to support async operations."
@@ -49,7 +55,7 @@ def _make_fs(self) -> tuple[AsyncFileSystem, str]:
4955
return fs, root
5056

5157
async def get(
52-
self, key: str, byte_range: tuple[int, int | None] | None = None
58+
self, key: str, byte_range: tuple[int | None, int | None] | None = None
5359
) -> Buffer | None:
5460
assert isinstance(key, str)
5561
fs, root = self._make_fs()
@@ -67,6 +73,7 @@ async def get(
6773
return value
6874

6975
async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None = None) -> None:
76+
self._check_writable()
7077
assert isinstance(key, str)
7178
fs, root = self._make_fs()
7279
path = _dereference_path(root, key)
@@ -80,6 +87,7 @@ async def set(self, key: str, value: Buffer, byte_range: tuple[int, int] | None
8087
await fs._pipe_file(path, value)
8188

8289
async def delete(self, key: str) -> None:
90+
self._check_writable()
8391
fs, root = self._make_fs()
8492
path = _dereference_path(root, key)
8593
if await fs._exists(path):

0 commit comments

Comments
 (0)