Skip to content
Merged
1 change: 1 addition & 0 deletions changes/3138.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +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.
16 changes: 16 additions & 0 deletions src/zarr/abc/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,22 @@
await store._open()
return store

def with_read_only(self, read_only: bool = False) -> Store:
"""
Return a new store of the same type pointing to the same location with the specified read_only state.
The returned Store is not automatically opened.
Parameters
----------
read_only
If True, the store will be created in read-only mode. Defaults to False.
Returns
-------
A new store of the same type with the new read only attribute.
"""
raise NotImplementedError("with_read_only is not implemented for this store type.")

Check warning on line 100 in src/zarr/abc/store.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/abc/store.py#L100

Added line #L100 was not covered by tests

def __enter__(self) -> Self:
"""Enter a context manager that will close the store upon exiting."""
return self
Expand Down
10 changes: 10 additions & 0 deletions src/zarr/storage/_fsspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@

fs: AsyncFileSystem
allowed_exceptions: tuple[type[Exception], ...]
path: str

def __init__(
self,
Expand Down Expand Up @@ -258,6 +259,15 @@

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

def with_read_only(self, read_only: bool = False) -> FsspecStore:
# docstring inherited
return type(self)(

Check warning on line 264 in src/zarr/storage/_fsspec.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_fsspec.py#L264

Added line #L264 was not covered by tests
fs=self.fs,
path=self.path,
allowed_exceptions=self.allowed_exceptions,
read_only=read_only,
)

async def clear(self) -> None:
# docstring inherited
try:
Expand Down
7 changes: 7 additions & 0 deletions src/zarr/storage/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@
)
self.root = root

def with_read_only(self, read_only: bool = False) -> LocalStore:
# docstring inherited
return type(self)(

Check warning on line 107 in src/zarr/storage/_local.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_local.py#L107

Added line #L107 was not covered by tests
root=self.root,
read_only=read_only,
)

async def _open(self) -> None:
if not self.read_only:
self.root.mkdir(parents=True, exist_ok=True)
Expand Down
7 changes: 7 additions & 0 deletions src/zarr/storage/_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ def __init__(
store_dict = {}
self._store_dict = store_dict

def with_read_only(self, read_only: bool = False) -> MemoryStore:
# docstring inherited
return type(self)(
store_dict=self._store_dict,
read_only=read_only,
)

async def clear(self) -> None:
# docstring inherited
self._store_dict.clear()
Expand Down
7 changes: 7 additions & 0 deletions src/zarr/storage/_obstore.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
super().__init__(read_only=read_only)
self.store = store

def with_read_only(self, read_only: bool = False) -> ObjectStore:
# docstring inherited
return type(self)(

Check warning on line 74 in src/zarr/storage/_obstore.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/storage/_obstore.py#L74

Added line #L74 was not covered by tests
store=self.store,
read_only=read_only,
)

def __str__(self) -> str:
return f"object_store://{self.store}"

Expand Down
51 changes: 51 additions & 0 deletions src/zarr/testing/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,57 @@
):
await store.delete("foo")

async def test_with_read_only_store(self, open_kwargs: dict[str, Any]) -> None:
kwargs = {**open_kwargs, "read_only": True}
store = await self.store_cls.open(**kwargs)
assert store.read_only

# Test that you cannot write to a read-only store
with pytest.raises(
ValueError, match="store was opened in read-only mode and does not support writing"
):
await store.set("foo", self.buffer_cls.from_bytes(b"bar"))

# Check if the store implements with_read_only
try:
writer = store.with_read_only(read_only=False)

# Test that you can write to a new store copy
assert not writer._is_open
assert not writer.read_only
await writer.set("foo", self.buffer_cls.from_bytes(b"bar"))
await writer.delete("foo")

# Test that you cannot write to the original store
assert store.read_only
with pytest.raises(
ValueError, match="store was opened in read-only mode and does not support writing"
):
await store.set("foo", self.buffer_cls.from_bytes(b"bar"))
with pytest.raises(
ValueError, match="store was opened in read-only mode and does not support writing"
):
await store.delete("foo")

# Test that you cannot write to a read-only store copy
reader = store.with_read_only(read_only=True)
assert reader.read_only
with pytest.raises(
ValueError, match="store was opened in read-only mode and does not support writing"
):
await reader.set("foo", self.buffer_cls.from_bytes(b"bar"))
with pytest.raises(
ValueError, match="store was opened in read-only mode and does not support writing"
):
await reader.delete("foo")

except NotImplementedError:

Check warning on line 196 in src/zarr/testing/store.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L196

Added line #L196 was not covered by tests
# Test that stores that do not implement with_read_only raise NotImplementedError with the correct message
with pytest.raises(

Check warning on line 198 in src/zarr/testing/store.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L198

Added line #L198 was not covered by tests
NotImplementedError, match="with_read_only is not implemented for this store type."
):
store.with_read_only(read_only=False)

Check warning on line 201 in src/zarr/testing/store.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L201

Added line #L201 was not covered by tests

@pytest.mark.parametrize("key", ["c/0", "foo/c/0.0", "foo/0/0"])
@pytest.mark.parametrize(
("data", "byte_range"),
Expand Down