Skip to content

Commit 325786a

Browse files
brokkoli71jhamman
andauthored
fix file modes (#2000)
* fix file modes * change store mode from literal to class of properties * raise FileNotFoundError instead of KeyError * rename OpenMode to AccessMode * rename AccessMode parameters * enforce AccessMode for MemoryStore and RemoteStore * fix RemoteStore * fix RemoteStore * formatting * fix RemoteStore._exists * Revert "fix RemoteStore._exists" This reverts commit 5f876d2. * create async Store.open() * make Store.open() classmethod * async clear and root_exists in Store * fix test_remote.py:test_basic * fix RemoteStore.open * remove unnecessary import zarr in tests * rename root_exists to (not) empty, test and fix store.empty, store.clear * mypy * incorporate feedback on store._open() Co-authored-by: Joe Hamman <[email protected]> * rename store.ensure_open to store._ensure_open --------- Co-authored-by: Joe Hamman <[email protected]>
1 parent aef47ac commit 325786a

20 files changed

+278
-124
lines changed

src/zarr/abc/store.py

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,74 @@
11
from abc import ABC, abstractmethod
22
from collections.abc import AsyncGenerator
3-
from typing import Protocol, runtime_checkable
3+
from typing import Any, NamedTuple, Protocol, runtime_checkable
4+
5+
from typing_extensions import Self
46

57
from zarr.buffer import Buffer, BufferPrototype
6-
from zarr.common import BytesLike, OpenMode
8+
from zarr.common import AccessModeLiteral, BytesLike
9+
10+
11+
class AccessMode(NamedTuple):
12+
readonly: bool
13+
overwrite: bool
14+
create: bool
15+
update: bool
16+
17+
@classmethod
18+
def from_literal(cls, mode: AccessModeLiteral) -> Self:
19+
if mode in ("r", "r+", "a", "w", "w-"):
20+
return cls(
21+
readonly=mode == "r",
22+
overwrite=mode == "w",
23+
create=mode in ("a", "w", "w-"),
24+
update=mode in ("r+", "a"),
25+
)
26+
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")
727

828

929
class Store(ABC):
10-
_mode: OpenMode
30+
_mode: AccessMode
31+
_is_open: bool
32+
33+
def __init__(self, mode: AccessModeLiteral = "r", *args: Any, **kwargs: Any):
34+
self._is_open = False
35+
self._mode = AccessMode.from_literal(mode)
36+
37+
@classmethod
38+
async def open(cls, *args: Any, **kwargs: Any) -> Self:
39+
store = cls(*args, **kwargs)
40+
await store._open()
41+
return store
42+
43+
async def _open(self) -> None:
44+
if self._is_open:
45+
raise ValueError("store is already open")
46+
if not await self.empty():
47+
if self.mode.update or self.mode.readonly:
48+
pass
49+
elif self.mode.overwrite:
50+
await self.clear()
51+
else:
52+
raise FileExistsError("Store already exists")
53+
self._is_open = True
54+
55+
async def _ensure_open(self) -> None:
56+
if not self._is_open:
57+
await self._open()
1158

12-
def __init__(self, mode: OpenMode = "r"):
13-
if mode not in ("r", "r+", "w", "w-", "a"):
14-
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")
15-
self._mode = mode
59+
@abstractmethod
60+
async def empty(self) -> bool: ...
61+
62+
@abstractmethod
63+
async def clear(self) -> None: ...
1664

1765
@property
18-
def mode(self) -> OpenMode:
66+
def mode(self) -> AccessMode:
1967
"""Access mode of the store."""
2068
return self._mode
2169

22-
@property
23-
def writeable(self) -> bool:
24-
"""Is the store writeable?"""
25-
return self.mode in ("a", "w", "w-")
26-
2770
def _check_writable(self) -> None:
28-
if not self.writeable:
71+
if self.mode.readonly:
2972
raise ValueError("store mode does not support writing")
3073

3174
@abstractmethod
@@ -173,8 +216,9 @@ def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
173216
"""
174217
...
175218

176-
def close(self) -> None: # noqa: B027
219+
def close(self) -> None:
177220
"""Close the store."""
221+
self._is_open = False
178222
pass
179223

180224

src/zarr/api/asynchronous.py

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from zarr.array import Array, AsyncArray
1313
from zarr.buffer import NDArrayLike
1414
from zarr.chunk_key_encodings import ChunkKeyEncoding
15-
from zarr.common import JSON, ChunkCoords, MemoryOrder, OpenMode, ZarrFormat
15+
from zarr.common import JSON, AccessModeLiteral, ChunkCoords, MemoryOrder, ZarrFormat
1616
from zarr.group import AsyncGroup
1717
from zarr.metadata import ArrayV2Metadata, ArrayV3Metadata
1818
from zarr.store import (
@@ -158,7 +158,7 @@ async def load(
158158
async def open(
159159
*,
160160
store: StoreLike | None = None,
161-
mode: OpenMode | None = None, # type and value changed
161+
mode: AccessModeLiteral | None = None, # type and value changed
162162
zarr_version: ZarrFormat | None = None, # deprecated
163163
zarr_format: ZarrFormat | None = None,
164164
path: str | None = None,
@@ -189,15 +189,15 @@ async def open(
189189
Return type depends on what exists in the given store.
190190
"""
191191
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
192-
store_path = make_store_path(store, mode=mode)
192+
store_path = await make_store_path(store, mode=mode)
193193

194194
if path is not None:
195195
store_path = store_path / path
196196

197197
try:
198-
return await open_array(store=store_path, zarr_format=zarr_format, **kwargs)
198+
return await open_array(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
199199
except KeyError:
200-
return await open_group(store=store_path, zarr_format=zarr_format, **kwargs)
200+
return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
201201

202202

203203
async def open_consolidated(*args: Any, **kwargs: Any) -> AsyncGroup:
@@ -267,7 +267,7 @@ async def save_array(
267267
or _default_zarr_version()
268268
)
269269

270-
store_path = make_store_path(store, mode="w")
270+
store_path = await make_store_path(store, mode="w")
271271
if path is not None:
272272
store_path = store_path / path
273273
new = await AsyncArray.create(
@@ -421,7 +421,7 @@ async def group(
421421
or _default_zarr_version()
422422
)
423423

424-
store_path = make_store_path(store)
424+
store_path = await make_store_path(store)
425425
if path is not None:
426426
store_path = store_path / path
427427

@@ -451,7 +451,7 @@ async def group(
451451
async def open_group(
452452
*, # Note: this is a change from v2
453453
store: StoreLike | None = None,
454-
mode: OpenMode | None = None, # not used
454+
mode: AccessModeLiteral | None = None, # not used
455455
cache_attrs: bool | None = None, # not used, default changed
456456
synchronizer: Any = None, # not used
457457
path: str | None = None,
@@ -512,7 +512,7 @@ async def open_group(
512512
if storage_options is not None:
513513
warnings.warn("storage_options is not yet implemented", RuntimeWarning, stacklevel=2)
514514

515-
store_path = make_store_path(store, mode=mode)
515+
store_path = await make_store_path(store, mode=mode)
516516
if path is not None:
517517
store_path = store_path / path
518518

@@ -682,8 +682,8 @@ async def create(
682682
if meta_array is not None:
683683
warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2)
684684

685-
mode = cast(OpenMode, "r" if read_only else "w")
686-
store_path = make_store_path(store, mode=mode)
685+
mode = kwargs.pop("mode", cast(AccessModeLiteral, "r" if read_only else "w"))
686+
store_path = await make_store_path(store, mode=mode)
687687
if path is not None:
688688
store_path = store_path / path
689689

@@ -854,22 +854,24 @@ async def open_array(
854854
The opened array.
855855
"""
856856

857-
store_path = make_store_path(store)
857+
store_path = await make_store_path(store)
858858
if path is not None:
859859
store_path = store_path / path
860860

861861
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
862862

863863
try:
864864
return await AsyncArray.open(store_path, zarr_format=zarr_format)
865-
except KeyError as e:
866-
if store_path.store.writeable:
867-
pass
868-
else:
869-
raise e
870-
871-
# if array was not found, create it
872-
return await create(store=store, path=path, zarr_format=zarr_format, **kwargs)
865+
except FileNotFoundError as e:
866+
if store_path.store.mode.create:
867+
return await create(
868+
store=store_path,
869+
path=path,
870+
zarr_format=zarr_format,
871+
overwrite=store_path.store.mode.overwrite,
872+
**kwargs,
873+
)
874+
raise e
873875

874876

875877
async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AsyncArray:

src/zarr/api/synchronous.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import zarr.api.asynchronous as async_api
66
from zarr.array import Array, AsyncArray
77
from zarr.buffer import NDArrayLike
8-
from zarr.common import JSON, ChunkCoords, OpenMode, ZarrFormat
8+
from zarr.common import JSON, AccessModeLiteral, ChunkCoords, ZarrFormat
99
from zarr.group import Group
1010
from zarr.store import StoreLike
1111
from zarr.sync import sync
@@ -36,7 +36,7 @@ def load(
3636
def open(
3737
*,
3838
store: StoreLike | None = None,
39-
mode: OpenMode | None = None, # type and value changed
39+
mode: AccessModeLiteral | None = None, # type and value changed
4040
zarr_version: ZarrFormat | None = None, # deprecated
4141
zarr_format: ZarrFormat | None = None,
4242
path: str | None = None,
@@ -161,7 +161,7 @@ def group(
161161
def open_group(
162162
*, # Note: this is a change from v2
163163
store: StoreLike | None = None,
164-
mode: OpenMode | None = None, # not used in async api
164+
mode: AccessModeLiteral | None = None, # not used in async api
165165
cache_attrs: bool | None = None, # default changed, not used in async api
166166
synchronizer: Any = None, # not used in async api
167167
path: str | None = None,

src/zarr/array.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ async def create(
142142
exists_ok: bool = False,
143143
data: npt.ArrayLike | None = None,
144144
) -> AsyncArray:
145-
store_path = make_store_path(store)
145+
store_path = await make_store_path(store)
146146

147147
if chunk_shape is None:
148148
if chunks is None:
@@ -334,18 +334,18 @@ async def open(
334334
store: StoreLike,
335335
zarr_format: ZarrFormat | None = 3,
336336
) -> AsyncArray:
337-
store_path = make_store_path(store)
337+
store_path = await make_store_path(store)
338338

339339
if zarr_format == 2:
340340
zarray_bytes, zattrs_bytes = await gather(
341341
(store_path / ZARRAY_JSON).get(), (store_path / ZATTRS_JSON).get()
342342
)
343343
if zarray_bytes is None:
344-
raise KeyError(store_path) # filenotfounderror?
344+
raise FileNotFoundError(store_path)
345345
elif zarr_format == 3:
346346
zarr_json_bytes = await (store_path / ZARR_JSON).get()
347347
if zarr_json_bytes is None:
348-
raise KeyError(store_path) # filenotfounderror?
348+
raise FileNotFoundError(store_path)
349349
elif zarr_format is None:
350350
zarr_json_bytes, zarray_bytes, zattrs_bytes = await gather(
351351
(store_path / ZARR_JSON).get(),
@@ -357,7 +357,7 @@ async def open(
357357
# alternatively, we could warn and favor v3
358358
raise ValueError("Both zarr.json and .zarray objects exist")
359359
if zarr_json_bytes is None and zarray_bytes is None:
360-
raise KeyError(store_path) # filenotfounderror?
360+
raise FileNotFoundError(store_path)
361361
# set zarr_format based on which keys were found
362362
if zarr_json_bytes is not None:
363363
zarr_format = 3
@@ -412,7 +412,7 @@ def attrs(self) -> dict[str, JSON]:
412412

413413
@property
414414
def read_only(self) -> bool:
415-
return bool(not self.store_path.store.writeable)
415+
return self.store_path.store.mode.readonly
416416

417417
@property
418418
def path(self) -> str:

src/zarr/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
ZarrFormat = Literal[2, 3]
3434
JSON = None | str | int | float | Enum | dict[str, "JSON"] | list["JSON"] | tuple["JSON", ...]
3535
MemoryOrder = Literal["C", "F"]
36-
OpenMode = Literal["r", "r+", "a", "w", "w-"]
36+
AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"]
3737

3838

3939
def product(tup: ChunkCoords) -> int:

src/zarr/group.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ async def create(
129129
exists_ok: bool = False,
130130
zarr_format: ZarrFormat = 3,
131131
) -> AsyncGroup:
132-
store_path = make_store_path(store)
132+
store_path = await make_store_path(store)
133133
if not exists_ok:
134134
await ensure_no_existing_node(store_path, zarr_format=zarr_format)
135135
attributes = attributes or {}
@@ -146,7 +146,7 @@ async def open(
146146
store: StoreLike,
147147
zarr_format: Literal[2, 3, None] = 3,
148148
) -> AsyncGroup:
149-
store_path = make_store_path(store)
149+
store_path = await make_store_path(store)
150150

151151
if zarr_format == 2:
152152
zgroup_bytes, zattrs_bytes = await asyncio.gather(
@@ -169,7 +169,7 @@ async def open(
169169
# alternatively, we could warn and favor v3
170170
raise ValueError("Both zarr.json and .zgroup objects exist")
171171
if zarr_json_bytes is None and zgroup_bytes is None:
172-
raise KeyError(store_path) # filenotfounderror?
172+
raise FileNotFoundError(store_path)
173173
# set zarr_format based on which keys were found
174174
if zarr_json_bytes is not None:
175175
zarr_format = 3

src/zarr/store/core.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from pathlib import Path
55
from typing import Any, Literal
66

7-
from zarr.abc.store import Store
7+
from zarr.abc.store import AccessMode, Store
88
from zarr.buffer import Buffer, BufferPrototype, default_buffer_prototype
9-
from zarr.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, OpenMode, ZarrFormat
9+
from zarr.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, AccessModeLiteral, ZarrFormat
1010
from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError
1111
from zarr.store.local import LocalStore
1212
from zarr.store.memory import MemoryStore
@@ -68,23 +68,26 @@ def __eq__(self, other: Any) -> bool:
6868
StoreLike = Store | StorePath | Path | str
6969

7070

71-
def make_store_path(store_like: StoreLike | None, *, mode: OpenMode | None = None) -> StorePath:
71+
async def make_store_path(
72+
store_like: StoreLike | None, *, mode: AccessModeLiteral | None = None
73+
) -> StorePath:
7274
if isinstance(store_like, StorePath):
7375
if mode is not None:
74-
assert mode == store_like.store.mode
76+
assert AccessMode.from_literal(mode) == store_like.store.mode
7577
return store_like
7678
elif isinstance(store_like, Store):
7779
if mode is not None:
78-
assert mode == store_like.mode
80+
assert AccessMode.from_literal(mode) == store_like.mode
81+
await store_like._ensure_open()
7982
return StorePath(store_like)
8083
elif store_like is None:
8184
if mode is None:
8285
mode = "w" # exception to the default mode = 'r'
83-
return StorePath(MemoryStore(mode=mode))
86+
return StorePath(await MemoryStore.open(mode=mode))
8487
elif isinstance(store_like, Path):
85-
return StorePath(LocalStore(store_like, mode=mode or "r"))
88+
return StorePath(await LocalStore.open(root=store_like, mode=mode or "r"))
8689
elif isinstance(store_like, str):
87-
return StorePath(LocalStore(Path(store_like), mode=mode or "r"))
90+
return StorePath(await LocalStore.open(root=Path(store_like), mode=mode or "r"))
8891
raise TypeError
8992

9093

0 commit comments

Comments
 (0)