Skip to content

Commit 6f52bfb

Browse files
committed
Create read only copy if needed when opening a store path
1 parent 286bef8 commit 6f52bfb

3 files changed

Lines changed: 60 additions & 27 deletions

File tree

src/zarr/storage/_common.py

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import importlib.util
44
import json
5+
import warnings
56
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias
7+
from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias, get_args
78

89
from zarr.abc.store import ByteRequest, Store
910
from zarr.core.buffer import Buffer, default_buffer_prototype
@@ -54,59 +55,88 @@ def __init__(self, store: Store, path: str = "") -> None:
5455
def read_only(self) -> bool:
5556
return self.store.read_only
5657

58+
@classmethod
59+
async def _create_open_instance(cls, store: Store, path: str) -> Self:
60+
"""Helper to create and return a StorePath instance."""
61+
await store._ensure_open()
62+
return cls(store, path)
63+
5764
@classmethod
5865
async def open(cls, store: Store, path: str, mode: AccessModeLiteral | None = None) -> Self:
5966
"""
6067
Open StorePath based on the provided mode.
6168
62-
* If the mode is 'w-' and the StorePath contains keys, raise a FileExistsError.
63-
* If the mode is 'w', delete all keys nested within the StorePath
64-
* If the mode is 'a', 'r', or 'r+', do nothing
69+
* If the mode is None, return an opened version of the store with no changes.
70+
* If the mode is 'r+', 'w-', 'w', or 'a' and the store is read-only, raise a ValueError.
71+
* If the mode is 'r' and the store is not read-only, return a copy of the store with read_only set to True.
72+
* If the mode is 'w-' and the store is not read-only and the StorePath contains keys, raise a FileExistsError.
73+
* If the mode is 'w' and the store is not read-only, delete all keys nested within the StorePath.
6574
6675
Parameters
6776
----------
6877
mode : AccessModeLiteral
6978
The mode to use when initializing the store path.
7079
80+
The accepted values are:
81+
82+
- ``'r'``: read only (must exist)
83+
- ``'r+'``: read/write (must exist)
84+
- ``'a'``: read/write (create if doesn't exist)
85+
- ``'w'``: read/write (overwrite if exists)
86+
- ``'w-'``: read/write (create if doesn't exist).
87+
7188
Raises
7289
------
7390
FileExistsError
7491
If the mode is 'w-' and the store path already exists.
75-
ValueError
76-
If the mode is not "r" and the store is read-only, or
77-
if the mode is "r" and the store is not read-only.
7892
"""
7993

80-
await store._ensure_open()
81-
self = cls(store, path)
82-
8394
# fastpath if mode is None
8495
if mode is None:
85-
return self
96+
return await cls._create_open_instance(store, path)
8697

87-
if store.read_only and mode != "r":
88-
raise ValueError(f"Store is read-only but mode is '{mode}'")
89-
if not store.read_only and mode == "r":
90-
raise ValueError(f"Store is not read-only but mode is '{mode}'")
98+
if mode not in get_args(AccessModeLiteral):
99+
raise ValueError(f"Invalid mode: {mode}, expected one of {AccessModeLiteral}")
91100

101+
if store.read_only:
102+
# Don't allow write operations on a read-only store
103+
if mode != "r":
104+
raise ValueError(
105+
f"Store is read-only but mode is '{mode}'. Create a writable store or use 'r' mode."
106+
)
107+
self = await cls._create_open_instance(store, path)
108+
elif mode == "r":
109+
# Create read-only copy for read mode on writable store
110+
try:
111+
warnings.warn(
112+
"Store is not read-only but mode is 'r'. Creating a read-only copy. "
113+
"This behavior may change in the future with a more granular permissions model.",
114+
UserWarning,
115+
stacklevel=2,
116+
)
117+
self = await cls._create_open_instance(store.with_read_only(True), path)
118+
except NotImplementedError as e:
119+
raise ValueError(
120+
"Store is not read-only but mode is 'r'. Unable to create a read-only copy of the store."
121+
) from e
122+
else:
123+
# writable store and writable mode
124+
await store._ensure_open()
125+
self = await cls._create_open_instance(store, path)
126+
127+
# Handle mode-specific operations
92128
match mode:
93129
case "w-":
94130
if not await self.is_empty():
95-
msg = (
96-
f"{self} is not empty, but `mode` is set to 'w-'."
97-
"Either remove the existing objects in storage,"
98-
"or set `mode` to a value that handles pre-existing objects"
99-
"in storage, like `a` or `w`."
131+
raise FileExistsError(
132+
f"Cannot create '{path}' with mode 'w-' because it already contains data. "
133+
f"Use mode 'w' to overwrite or 'a' to append."
100134
)
101-
raise FileExistsError(msg)
102135
case "w":
103136
await self.delete_dir()
104137
case "a" | "r" | "r+":
105138
# No init action
106139
pass
107-
case _:
108-
raise ValueError(f"Invalid mode: {mode}")
109-
110140
return self
111141

112142
async def get(

tests/test_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1318,7 +1318,7 @@ def test_no_overwrite_open(tmp_path: Path, open_func: Callable, mode: str) -> No
13181318
existing_fpath = add_empty_file(tmp_path)
13191319

13201320
assert existing_fpath.exists()
1321-
with contextlib.suppress(FileExistsError, FileNotFoundError, ValueError):
1321+
with contextlib.suppress(FileExistsError, FileNotFoundError, UserWarning):
13221322
open_func(store=store, mode=mode)
13231323
if mode == "w":
13241324
assert not existing_fpath.exists()

tests/test_store/test_core.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,11 @@ def test_relativize_path_invalid() -> None:
263263
_relativize_path(path="a/b/c", prefix="b")
264264

265265

266-
def test_invalid_open_mode() -> None:
266+
def test_different_open_mode() -> None:
267267
store = MemoryStore()
268268
zarr.create((100,), store=store, zarr_format=2, path="a")
269-
with pytest.raises(ValueError, match="Store is not read-only but mode is 'r'"):
269+
with pytest.warns(
270+
UserWarning,
271+
match="Store is not read-only but mode is 'r'. Attempting to create a read-only copy. This behavior may change in the future with a more granular permissions model.",
272+
):
270273
zarr.open_array(store=store, path="a", zarr_format=2, mode="r")

0 commit comments

Comments
 (0)