Skip to content

Commit 0e28404

Browse files
dstansbyd-v-b
andauthored
Prevent mode='r+' from creating new directories (#3307)
* Prevent mode='r+' from creating new directories * Add test * Open store for mode = r+ * Add changelog * Fix errr raised when opening non-existent path --------- Co-authored-by: Davis Bennett <[email protected]>
1 parent dc641fe commit 0e28404

File tree

4 files changed

+52
-8
lines changed

4 files changed

+52
-8
lines changed

changes/3307.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Opening an array or group with ``mode="r+"`` will no longer create new arrays or groups.

src/zarr/storage/_common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ async def make_store_path(
358358

359359
elif isinstance(store_like, Path):
360360
# Create a new LocalStore
361-
store = await LocalStore.open(root=store_like, read_only=_read_only)
361+
store = await LocalStore.open(root=store_like, mode=mode)
362362

363363
elif isinstance(store_like, str):
364364
# Either a FSSpec URI or a local filesystem path

src/zarr/storage/_local.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import os
66
import shutil
77
from pathlib import Path
8-
from typing import TYPE_CHECKING
8+
from typing import TYPE_CHECKING, Self
99

1010
from zarr.abc.store import (
1111
ByteRequest,
@@ -16,7 +16,7 @@
1616
)
1717
from zarr.core.buffer import Buffer
1818
from zarr.core.buffer.core import default_buffer_prototype
19-
from zarr.core.common import concurrent_map
19+
from zarr.core.common import AccessModeLiteral, concurrent_map
2020

2121
if TYPE_CHECKING:
2222
from collections.abc import AsyncIterator, Iterable
@@ -102,16 +102,56 @@ 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:
105+
def with_read_only(self, read_only: bool = False) -> Self:
106106
# docstring inherited
107107
return type(self)(
108108
root=self.root,
109109
read_only=read_only,
110110
)
111111

112-
async def _open(self) -> None:
112+
@classmethod
113+
async def open(
114+
cls, root: Path | str, *, read_only: bool = False, mode: AccessModeLiteral | None = None
115+
) -> Self:
116+
"""
117+
Create and open the store.
118+
119+
Parameters
120+
----------
121+
root : str or Path
122+
Directory to use as root of store.
123+
read_only : bool
124+
Whether the store is read-only
125+
mode :
126+
Mode in which to create the store. This only affects opening the store,
127+
and the final read-only state of the store is controlled through the
128+
read_only parameter.
129+
130+
Returns
131+
-------
132+
Store
133+
The opened store instance.
134+
"""
135+
# If mode = 'r+', want to open in read only mode (fail if exists),
136+
# but return a writeable store
137+
if mode is not None:
138+
read_only_creation = mode in ["r", "r+"]
139+
else:
140+
read_only_creation = read_only
141+
store = cls(root, read_only=read_only_creation)
142+
await store._open()
143+
144+
# Set read_only state
145+
store = store.with_read_only(read_only)
146+
await store._open()
147+
return store
148+
149+
async def _open(self, *, mode: AccessModeLiteral | None = None) -> None:
113150
if not self.read_only:
114151
self.root.mkdir(parents=True, exist_ok=True)
152+
153+
if not self.root.exists():
154+
raise FileNotFoundError(f"{self.root} does not exist")
115155
return await super()._open()
116156

117157
async def clear(self) -> None:

tests/test_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
from zarr.errors import (
4646
ArrayNotFoundError,
4747
MetadataValidationError,
48-
NodeNotFoundError,
4948
ZarrDeprecationWarning,
5049
ZarrUserWarning,
5150
)
@@ -191,7 +190,7 @@ async def test_open_array(memory_store: MemoryStore, zarr_format: ZarrFormat) ->
191190
assert z.read_only
192191

193192
# path not found
194-
with pytest.raises(NodeNotFoundError):
193+
with pytest.raises(FileNotFoundError):
195194
zarr.api.synchronous.open(store="doesnotexist", mode="r", zarr_format=zarr_format)
196195

197196

@@ -333,8 +332,12 @@ def test_open_with_mode_r(tmp_path: Path) -> None:
333332

334333
def test_open_with_mode_r_plus(tmp_path: Path) -> None:
335334
# 'r+' means read/write (must exist)
335+
new_store_path = tmp_path / "new_store.zarr"
336+
assert not new_store_path.exists(), "Test should operate on non-existent directory"
336337
with pytest.raises(FileNotFoundError):
337-
zarr.open(store=tmp_path, mode="r+")
338+
zarr.open(store=new_store_path, mode="r+")
339+
assert not new_store_path.exists(), "mode='r+' should not create directory"
340+
338341
zarr.ones(store=tmp_path, shape=(3, 3))
339342
z2 = zarr.open(store=tmp_path, mode="r+")
340343
assert isinstance(z2, Array)

0 commit comments

Comments
 (0)