Skip to content

Commit da232ae

Browse files
d-v-bclaude
andcommitted
feat: add SupportsSetRange protocol and store implementations
Add SupportsSetRange protocol for stores that support writing to a byte range within an existing value (set_range/set_range_sync). Implement in MemoryStore and LocalStore, both explicitly subclassing the protocol. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e6207b7 commit da232ae

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

src/zarr/abc/store.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"Store",
2323
"SupportsDeleteSync",
2424
"SupportsGetSync",
25+
"SupportsSetRange",
2526
"SupportsSetSync",
2627
"SupportsSyncStore",
2728
"set_or_delete",
@@ -709,6 +710,15 @@ async def delete(self) -> None: ...
709710
async def set_if_not_exists(self, default: Buffer) -> None: ...
710711

711712

713+
@runtime_checkable
714+
class SupportsSetRange(Protocol):
715+
"""Protocol for stores that support writing to a byte range within an existing value."""
716+
717+
async def set_range(self, key: str, value: Buffer, start: int) -> None: ...
718+
719+
def set_range_sync(self, key: str, value: Buffer, start: int) -> None: ...
720+
721+
712722
@runtime_checkable
713723
class SupportsGetSync(Protocol):
714724
def get_sync(

src/zarr/storage/_local.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
RangeByteRequest,
1717
Store,
1818
SuffixByteRequest,
19+
SupportsSetRange,
1920
)
2021
from zarr.core.buffer import Buffer
2122
from zarr.core.buffer.core import default_buffer_prototype
@@ -77,6 +78,13 @@ def _atomic_write(
7778
raise
7879

7980

81+
def _put_range(path: Path, value: Buffer, start: int) -> None:
82+
"""Write bytes at a specific offset within an existing file."""
83+
with path.open("r+b") as f:
84+
f.seek(start)
85+
f.write(value.as_numpy_array().tobytes())
86+
87+
8088
def _put(path: Path, value: Buffer, exclusive: bool = False) -> int:
8189
path.parent.mkdir(parents=True, exist_ok=True)
8290
# write takes any object supporting the buffer protocol
@@ -85,7 +93,7 @@ def _put(path: Path, value: Buffer, exclusive: bool = False) -> int:
8593
return f.write(view)
8694

8795

88-
class LocalStore(Store):
96+
class LocalStore(Store, SupportsSetRange):
8997
"""
9098
Store for the local file system.
9199
@@ -292,6 +300,19 @@ async def _set(self, key: str, value: Buffer, exclusive: bool = False) -> None:
292300
path = self.root / key
293301
await asyncio.to_thread(_put, path, value, exclusive=exclusive)
294302

303+
async def set_range(self, key: str, value: Buffer, start: int) -> None:
304+
if not self._is_open:
305+
await self._open()
306+
self._check_writable()
307+
path = self.root / key
308+
await asyncio.to_thread(_put_range, path, value, start)
309+
310+
def set_range_sync(self, key: str, value: Buffer, start: int) -> None:
311+
self._ensure_open_sync()
312+
self._check_writable()
313+
path = self.root / key
314+
_put_range(path, value, start)
315+
295316
async def delete(self, key: str) -> None:
296317
"""
297318
Remove a key from the store.

src/zarr/storage/_memory.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from logging import getLogger
44
from typing import TYPE_CHECKING, Any, Self
55

6-
from zarr.abc.store import ByteRequest, Store
6+
from zarr.abc.store import ByteRequest, Store, SupportsSetRange
77
from zarr.core.buffer import Buffer, gpu
88
from zarr.core.buffer.core import default_buffer_prototype
99
from zarr.core.common import concurrent_map
@@ -18,7 +18,7 @@
1818
logger = getLogger(__name__)
1919

2020

21-
class MemoryStore(Store):
21+
class MemoryStore(Store, SupportsSetRange):
2222
"""
2323
Store for local memory.
2424
@@ -186,6 +186,26 @@ async def delete(self, key: str) -> None:
186186
except KeyError:
187187
logger.debug("Key %s does not exist.", key)
188188

189+
def _set_range_impl(self, key: str, value: Buffer, start: int) -> None:
190+
buf = self._store_dict[key]
191+
target = buf.as_numpy_array()
192+
if not target.flags.writeable:
193+
target = target.copy()
194+
self._store_dict[key] = buf.__class__(target)
195+
source = value.as_numpy_array()
196+
target[start : start + len(source)] = source
197+
198+
async def set_range(self, key: str, value: Buffer, start: int) -> None:
199+
self._check_writable()
200+
await self._ensure_open()
201+
self._set_range_impl(key, value, start)
202+
203+
def set_range_sync(self, key: str, value: Buffer, start: int) -> None:
204+
self._check_writable()
205+
if not self._is_open:
206+
self._is_open = True
207+
self._set_range_impl(key, value, start)
208+
189209
async def list(self) -> AsyncIterator[str]:
190210
# docstring inherited
191211
for key in self._store_dict:

0 commit comments

Comments
 (0)