Skip to content

Commit c2bb916

Browse files
authored
Merge branch 'main' into fix/consistent-create-array-signature
2 parents acec9ec + 2911be8 commit c2bb916

11 files changed

Lines changed: 136 additions & 24 deletions

File tree

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.

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ exclude = [
77
"/.github",
88
"/bench",
99
"/docs",
10-
"/notebooks"
1110
]
1211

1312
[project]
@@ -282,7 +281,6 @@ extend-exclude = [
282281
"buck-out",
283282
"build",
284283
"dist",
285-
"notebooks", # temporary, until we achieve compatibility with ruff ≥ 0.6
286284
"venv",
287285
"docs",
288286
"tests/test_regression/scripts/", # these are scripts that use a different version of python
@@ -384,7 +382,6 @@ module = [
384382
"tests.test_indexing",
385383
"tests.test_properties",
386384
"tests.test_sync",
387-
"tests.test_v2",
388385
"tests.test_regression.scripts.*"
389386
]
390387
ignore_errors = true

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/core/dtype/npy/structured.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,10 @@ def to_json(
162162
[f_name, f_dtype.to_json(zarr_format=zarr_format)] # type: ignore[list-item]
163163
for f_name, f_dtype in self.fields
164164
]
165-
base_dict = {"name": self._zarr_v3_name}
166-
base_dict["configuration"] = {"fields": fields} # type: ignore[assignment]
165+
base_dict = {
166+
"name": self._zarr_v3_name,
167+
"configuration": {"fields": fields},
168+
}
167169
return cast("DTypeSpec_V3", base_dict)
168170
raise ValueError(f"zarr_format must be 2 or 3, got {zarr_format}") # pragma: no cover
169171

src/zarr/core/dtype/wrapper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757

5858

5959
@dataclass(frozen=True, kw_only=True, slots=True)
60-
class ZDType(Generic[TDType_co, TScalar_co], ABC):
60+
class ZDType(ABC, Generic[TDType_co, TScalar_co]):
6161
"""
6262
Abstract base class for wrapping native array data types, e.g. numpy dtypes
6363

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)