Skip to content

Add with_read_only() convenience method to store #3138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 19, 2025
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.
21 changes: 21 additions & 0 deletions src/zarr/abc/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@
await store._open()
return store

def with_read_only(self, read_only: bool = False) -> Store:
"""
Return a new store with a new read_only setting.

The new store points to the same location with the specified new read_only state.
The returned Store is not automatically opened, and this store is
not automatically closed.

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(

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

View check run for this annotation

Codecov / codecov/patch

src/zarr/abc/store.py#L103

Added line #L103 was not covered by tests
f"with_read_only is not implemented for the {type(self)} store type."
)

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
52 changes: 52 additions & 0 deletions src/zarr/testing/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,58 @@
):
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)
except NotImplementedError:

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

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L166

Added line #L166 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 168 in src/zarr/testing/store.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L168

Added line #L168 was not covered by tests
NotImplementedError,
match=f"with_read_only is not implemented for the {type(store)} store type.",
):
store.with_read_only(read_only=False)
return

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

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/store.py#L172-L173

Added lines #L172 - L173 were not covered by tests

# 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")

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