Skip to content

Commit 04a4c5f

Browse files
Backport PR #3138: Add with_read_only() convenience method to store (#3181)
Co-authored-by: Max Jones <[email protected]>
1 parent e6bb1ae commit 04a4c5f

File tree

7 files changed

+105
-0
lines changed

7 files changed

+105
-0
lines changed

changes/3138.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adds a `with_read_only` convenience method to the `Store` abstract base class (raises `NotImplementedError`) and implementations to the `MemoryStore`, `ObjectStore`, `LocalStore`, and `FsspecStore` classes.

src/zarr/abc/store.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,27 @@ async def open(cls, *args: Any, **kwargs: Any) -> Self:
8383
await store._open()
8484
return store
8585

86+
def with_read_only(self, read_only: bool = False) -> Store:
87+
"""
88+
Return a new store with a new read_only setting.
89+
90+
The new store points to the same location with the specified new read_only state.
91+
The returned Store is not automatically opened, and this store is
92+
not automatically closed.
93+
94+
Parameters
95+
----------
96+
read_only
97+
If True, the store will be created in read-only mode. Defaults to False.
98+
99+
Returns
100+
-------
101+
A new store of the same type with the new read only attribute.
102+
"""
103+
raise NotImplementedError(
104+
f"with_read_only is not implemented for the {type(self)} store type."
105+
)
106+
86107
def __enter__(self) -> Self:
87108
"""Enter a context manager that will close the store upon exiting."""
88109
return self

src/zarr/storage/_fsspec.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class FsspecStore(Store):
122122

123123
fs: AsyncFileSystem
124124
allowed_exceptions: tuple[type[Exception], ...]
125+
path: str
125126

126127
def __init__(
127128
self,
@@ -258,6 +259,15 @@ def from_url(
258259

259260
return cls(fs=fs, path=path, read_only=read_only, allowed_exceptions=allowed_exceptions)
260261

262+
def with_read_only(self, read_only: bool = False) -> FsspecStore:
263+
# docstring inherited
264+
return type(self)(
265+
fs=self.fs,
266+
path=self.path,
267+
allowed_exceptions=self.allowed_exceptions,
268+
read_only=read_only,
269+
)
270+
261271
async def clear(self) -> None:
262272
# docstring inherited
263273
try:

src/zarr/storage/_local.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ def __init__(self, root: Path | str, *, read_only: bool = False) -> None:
102102
)
103103
self.root = root
104104

105+
def with_read_only(self, read_only: bool = False) -> LocalStore:
106+
# docstring inherited
107+
return type(self)(
108+
root=self.root,
109+
read_only=read_only,
110+
)
111+
105112
async def _open(self) -> None:
106113
if not self.read_only:
107114
self.root.mkdir(parents=True, exist_ok=True)

src/zarr/storage/_memory.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ def __init__(
5454
store_dict = {}
5555
self._store_dict = store_dict
5656

57+
def with_read_only(self, read_only: bool = False) -> MemoryStore:
58+
# docstring inherited
59+
return type(self)(
60+
store_dict=self._store_dict,
61+
read_only=read_only,
62+
)
63+
5764
async def clear(self) -> None:
5865
# docstring inherited
5966
self._store_dict.clear()

src/zarr/storage/_obstore.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def __init__(self, store: _UpstreamObjectStore, *, read_only: bool = False) -> N
6969
super().__init__(read_only=read_only)
7070
self.store = store
7171

72+
def with_read_only(self, read_only: bool = False) -> ObjectStore:
73+
# docstring inherited
74+
return type(self)(
75+
store=self.store,
76+
read_only=read_only,
77+
)
78+
7279
def __str__(self) -> str:
7380
return f"object_store://{self.store}"
7481

src/zarr/testing/store.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,58 @@ async def test_read_only_store_raises(self, open_kwargs: dict[str, Any]) -> None
149149
):
150150
await store.delete("foo")
151151

152+
async def test_with_read_only_store(self, open_kwargs: dict[str, Any]) -> None:
153+
kwargs = {**open_kwargs, "read_only": True}
154+
store = await self.store_cls.open(**kwargs)
155+
assert store.read_only
156+
157+
# Test that you cannot write to a read-only store
158+
with pytest.raises(
159+
ValueError, match="store was opened in read-only mode and does not support writing"
160+
):
161+
await store.set("foo", self.buffer_cls.from_bytes(b"bar"))
162+
163+
# Check if the store implements with_read_only
164+
try:
165+
writer = store.with_read_only(read_only=False)
166+
except NotImplementedError:
167+
# Test that stores that do not implement with_read_only raise NotImplementedError with the correct message
168+
with pytest.raises(
169+
NotImplementedError,
170+
match=f"with_read_only is not implemented for the {type(store)} store type.",
171+
):
172+
store.with_read_only(read_only=False)
173+
return
174+
175+
# Test that you can write to a new store copy
176+
assert not writer._is_open
177+
assert not writer.read_only
178+
await writer.set("foo", self.buffer_cls.from_bytes(b"bar"))
179+
await writer.delete("foo")
180+
181+
# Test that you cannot write to the original store
182+
assert store.read_only
183+
with pytest.raises(
184+
ValueError, match="store was opened in read-only mode and does not support writing"
185+
):
186+
await store.set("foo", self.buffer_cls.from_bytes(b"bar"))
187+
with pytest.raises(
188+
ValueError, match="store was opened in read-only mode and does not support writing"
189+
):
190+
await store.delete("foo")
191+
192+
# Test that you cannot write to a read-only store copy
193+
reader = store.with_read_only(read_only=True)
194+
assert reader.read_only
195+
with pytest.raises(
196+
ValueError, match="store was opened in read-only mode and does not support writing"
197+
):
198+
await reader.set("foo", self.buffer_cls.from_bytes(b"bar"))
199+
with pytest.raises(
200+
ValueError, match="store was opened in read-only mode and does not support writing"
201+
):
202+
await reader.delete("foo")
203+
152204
@pytest.mark.parametrize("key", ["c/0", "foo/c/0.0", "foo/0/0"])
153205
@pytest.mark.parametrize(
154206
("data", "byte_range"),

0 commit comments

Comments
 (0)