Skip to content

Commit e49647b

Browse files
jhammand-v-bdcherian
authored
Refactor: store mode and initialization (#2442)
* fix(array): thread order parameter through to array __init__ * type fixes * move defaults * apply MemoryOrder type to ArrayV2Metadata * more more * more more more * feature(store,group,array): stores learn to delete prefixes when overwriting nodes - add Store.delete_dir and Store.delete_prefix - update array and group creation methods to call delete_dir - change list_prefix to return absolue keys * fixup * fixup * respond to review * fixup * fixup * fix(store): refactor store initialization to include only r or w permissions * Apply suggestions from code review Co-authored-by: Davis Bennett <[email protected]> * store -> readonly * fixups * fixups * fixups * fixup * readonly -> read_only * fixup * Fix hypothesis * fixup for windows * respond to reviews * is_empty --------- Co-authored-by: Davis Bennett <[email protected]> Co-authored-by: Deepak Cherian <[email protected]>
1 parent 3ab3607 commit e49647b

34 files changed

+399
-460
lines changed

docs/guide/storage.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Storage
22
=======
33

44
Zarr-Python supports multiple storage backends, including: local file systems,
5-
Zip files, remote stores via ``fspec`` (S3, HTTP, etc.), and in-memory stores. In
5+
Zip files, remote stores via ``fsspec`` (S3, HTTP, etc.), and in-memory stores. In
66
Zarr-Python 3, stores must implement the abstract store API from
77
:class:`zarr.abc.store.Store`.
88

@@ -19,9 +19,9 @@ to Zarr's top level API will result in the store being created automatically.
1919
.. code-block:: python
2020
2121
>>> import zarr
22-
>>> zarr.open("data/foo/bar", mode="r") # implicitly creates a LocalStore
22+
>>> zarr.open("data/foo/bar", mode="r") # implicitly creates a read-only LocalStore
2323
<Group file://data/foo/bar>
24-
>>> zarr.open("s3://foo/bar", mode="r") # implicitly creates a RemoteStore
24+
>>> zarr.open("s3://foo/bar", mode="r") # implicitly creates a read-only RemoteStore
2525
<Group s3://foo/bar>
2626
>>> data = {}
2727
>>> zarr.open(data, mode="w") # implicitly creates a MemoryStore
@@ -43,7 +43,7 @@ filesystem.
4343
.. code-block:: python
4444
4545
>>> import zarr
46-
>>> store = zarr.storage.LocalStore("data/foo/bar", mode="r")
46+
>>> store = zarr.storage.LocalStore("data/foo/bar", read_only=True)
4747
>>> zarr.open(store=store)
4848
<Group file://data/foo/bar>
4949
@@ -72,7 +72,7 @@ that implements the `AbstractFileSystem` API,
7272
.. code-block:: python
7373
7474
>>> import zarr
75-
>>> store = zarr.storage.RemoteStore.from_url("gs://foo/bar", mode="r")
75+
>>> store = zarr.storage.RemoteStore.from_url("gs://foo/bar", read_only=True)
7676
>>> zarr.open(store=store)
7777
<Array <RemoteStore(GCSFileSystem, foo/bar)> shape=(10, 20) dtype=float32>
7878
@@ -86,7 +86,7 @@ Zarr data (metadata and chunks) to a dictionary.
8686
8787
>>> import zarr
8888
>>> data = {}
89-
>>> store = zarr.storage.MemoryStore(data, mode="w")
89+
>>> store = zarr.storage.MemoryStore(data)
9090
>>> zarr.open(store=store, shape=(2, ))
9191
<Array memory://4943638848 shape=(2,) dtype=float64>
9292

src/zarr/abc/store.py

Lines changed: 32 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -3,72 +3,32 @@
33
from abc import ABC, abstractmethod
44
from asyncio import gather
55
from itertools import starmap
6-
from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable
6+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
77

88
if TYPE_CHECKING:
99
from collections.abc import AsyncGenerator, AsyncIterator, Iterable
1010
from types import TracebackType
1111
from typing import Any, Self, TypeAlias
1212

1313
from zarr.core.buffer import Buffer, BufferPrototype
14-
from zarr.core.common import AccessModeLiteral, BytesLike
14+
from zarr.core.common import BytesLike
1515

16-
__all__ = ["AccessMode", "ByteGetter", "ByteSetter", "Store", "set_or_delete"]
16+
__all__ = ["ByteGetter", "ByteSetter", "Store", "set_or_delete"]
1717

1818
ByteRangeRequest: TypeAlias = tuple[int | None, int | None]
1919

2020

21-
class AccessMode(NamedTuple):
22-
"""Access mode flags."""
23-
24-
str: AccessModeLiteral
25-
readonly: bool
26-
overwrite: bool
27-
create: bool
28-
update: bool
29-
30-
@classmethod
31-
def from_literal(cls, mode: AccessModeLiteral) -> Self:
32-
"""
33-
Create an AccessMode instance from a literal.
34-
35-
Parameters
36-
----------
37-
mode : AccessModeLiteral
38-
One of 'r', 'r+', 'w', 'w-', 'a'.
39-
40-
Returns
41-
-------
42-
AccessMode
43-
The created instance.
44-
45-
Raises
46-
------
47-
ValueError
48-
If mode is not one of 'r', 'r+', 'w', 'w-', 'a'.
49-
"""
50-
if mode in ("r", "r+", "a", "w", "w-"):
51-
return cls(
52-
str=mode,
53-
readonly=mode == "r",
54-
overwrite=mode == "w",
55-
create=mode in ("a", "w", "w-"),
56-
update=mode in ("r+", "a"),
57-
)
58-
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")
59-
60-
6121
class Store(ABC):
6222
"""
6323
Abstract base class for Zarr stores.
6424
"""
6525

66-
_mode: AccessMode
26+
_read_only: bool
6727
_is_open: bool
6828

69-
def __init__(self, *args: Any, mode: AccessModeLiteral = "r", **kwargs: Any) -> None:
29+
def __init__(self, *, read_only: bool = False) -> None:
7030
self._is_open = False
71-
self._mode = AccessMode.from_literal(mode)
31+
self._read_only = read_only
7232

7333
@classmethod
7434
async def open(cls, *args: Any, **kwargs: Any) -> Self:
@@ -112,81 +72,60 @@ async def _open(self) -> None:
11272
------
11373
ValueError
11474
If the store is already open.
115-
FileExistsError
116-
If ``mode='w-'`` and the store already exists.
117-
118-
Notes
119-
-----
120-
* When ``mode='w'`` and the store already exists, it will be cleared.
12175
"""
12276
if self._is_open:
12377
raise ValueError("store is already open")
124-
if self.mode.str == "w":
125-
await self.clear()
126-
elif self.mode.str == "w-" and not await self.empty():
127-
raise FileExistsError("Store already exists")
12878
self._is_open = True
12979

13080
async def _ensure_open(self) -> None:
13181
"""Open the store if it is not already open."""
13282
if not self._is_open:
13383
await self._open()
13484

135-
@abstractmethod
136-
async def empty(self) -> bool:
85+
async def is_empty(self, prefix: str) -> bool:
13786
"""
138-
Check if the store is empty.
87+
Check if the directory is empty.
88+
89+
Parameters
90+
----------
91+
prefix : str
92+
Prefix of keys to check.
13993
14094
Returns
14195
-------
14296
bool
14397
True if the store is empty, False otherwise.
14498
"""
145-
...
99+
if not self.supports_listing:
100+
raise NotImplementedError
101+
if prefix != "" and not prefix.endswith("/"):
102+
prefix += "/"
103+
async for _ in self.list_prefix(prefix):
104+
return False
105+
return True
146106

147-
@abstractmethod
148107
async def clear(self) -> None:
149108
"""
150109
Clear the store.
151110
152111
Remove all keys and values from the store.
153112
"""
154-
...
155-
156-
@abstractmethod
157-
def with_mode(self, mode: AccessModeLiteral) -> Self:
158-
"""
159-
Return a new store of the same type pointing to the same location with a new mode.
160-
161-
The returned Store is not automatically opened. Call :meth:`Store.open` before
162-
using.
163-
164-
Parameters
165-
----------
166-
mode : AccessModeLiteral
167-
The new mode to use.
168-
169-
Returns
170-
-------
171-
store
172-
A new store of the same type with the new mode.
173-
174-
Examples
175-
--------
176-
>>> writer = zarr.store.MemoryStore(mode="w")
177-
>>> reader = writer.with_mode("r")
178-
"""
179-
...
113+
if not self.supports_deletes:
114+
raise NotImplementedError
115+
if not self.supports_listing:
116+
raise NotImplementedError
117+
self._check_writable()
118+
await self.delete_dir("")
180119

181120
@property
182-
def mode(self) -> AccessMode:
183-
"""Access mode of the store."""
184-
return self._mode
121+
def read_only(self) -> bool:
122+
"""Is the store read-only?"""
123+
return self._read_only
185124

186125
def _check_writable(self) -> None:
187126
"""Raise an exception if the store is not writable."""
188-
if self.mode.readonly:
189-
raise ValueError("store mode does not support writing")
127+
if self.read_only:
128+
raise ValueError("store was opened in read-only mode and does not support writing")
190129

191130
@abstractmethod
192131
def __eq__(self, value: object) -> bool:
@@ -385,7 +324,7 @@ async def delete_dir(self, prefix: str) -> None:
385324
if not self.supports_listing:
386325
raise NotImplementedError
387326
self._check_writable()
388-
if not prefix.endswith("/"):
327+
if prefix != "" and not prefix.endswith("/"):
389328
prefix += "/"
390329
async for key in self.list_prefix(prefix):
391330
await self.delete(key)

0 commit comments

Comments
 (0)