Skip to content

fix file modes #2000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Jul 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
32e84eb
fix file modes
brokkoli71 Jun 27, 2024
6d66f2a
change store mode from literal to class of properties
brokkoli71 Jul 1, 2024
ab353c9
raise FileNotFoundError instead of KeyError
brokkoli71 Jul 1, 2024
9d83b6e
Merge branch 'refs/heads/master' into fix-file-modes
brokkoli71 Jul 1, 2024
a964cb5
rename OpenMode to AccessMode
brokkoli71 Jul 3, 2024
1ff5861
rename AccessMode parameters
brokkoli71 Jul 3, 2024
2340e8b
enforce AccessMode for MemoryStore and RemoteStore
brokkoli71 Jul 3, 2024
cab827c
fix RemoteStore
brokkoli71 Jul 3, 2024
b9f49b4
fix RemoteStore
brokkoli71 Jul 3, 2024
8b37537
Merge remote-tracking branch 'origin/fix-file-modes' into fix-file-modes
brokkoli71 Jul 3, 2024
1d46753
formatting
brokkoli71 Jul 3, 2024
5f876d2
fix RemoteStore._exists
brokkoli71 Jul 3, 2024
218a527
Revert "fix RemoteStore._exists"
brokkoli71 Jul 3, 2024
edd3630
create async Store.open()
brokkoli71 Jul 4, 2024
01e3fa2
make Store.open() classmethod
brokkoli71 Jul 4, 2024
f016d34
async clear and root_exists in Store
brokkoli71 Jul 4, 2024
6dbf994
fix test_remote.py:test_basic
brokkoli71 Jul 4, 2024
2c372aa
fix RemoteStore.open
brokkoli71 Jul 5, 2024
f921f9e
Merge branch 'refs/heads/master' into fix-file-modes
brokkoli71 Jul 5, 2024
f03e4da
remove unnecessary import zarr in tests
brokkoli71 Jul 18, 2024
0c12510
rename root_exists to (not) empty, test and fix store.empty, store.clear
brokkoli71 Jul 18, 2024
7932593
mypy
brokkoli71 Jul 18, 2024
ef0aef8
Merge branch 'v3' into fix-file-modes
brokkoli71 Jul 18, 2024
830dd7e
incorporate feedback on store._open()
brokkoli71 Jul 20, 2024
3abfad6
rename store.ensure_open to store._ensure_open
brokkoli71 Jul 20, 2024
7d75edc
Merge branch 'refs/heads/master' into fix-file-modes
brokkoli71 Jul 26, 2024
6616185
Merge branch 'refs/heads/master' into fix-file-modes
brokkoli71 Jul 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions src/zarr/abc/store.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,74 @@
from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator
from typing import Protocol, runtime_checkable
from typing import Any, NamedTuple, Protocol, runtime_checkable

from typing_extensions import Self

from zarr.buffer import Buffer, BufferPrototype
from zarr.common import BytesLike, OpenMode
from zarr.common import AccessModeLiteral, BytesLike


class AccessMode(NamedTuple):
readonly: bool
overwrite: bool
create: bool
update: bool

@classmethod
def from_literal(cls, mode: AccessModeLiteral) -> Self:
if mode in ("r", "r+", "a", "w", "w-"):
return cls(
readonly=mode == "r",
overwrite=mode == "w",
create=mode in ("a", "w", "w-"),
update=mode in ("r+", "a"),
)
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")


class Store(ABC):
_mode: OpenMode
_mode: AccessMode
_is_open: bool

def __init__(self, mode: AccessModeLiteral = "r", *args: Any, **kwargs: Any):
self._is_open = False
self._mode = AccessMode.from_literal(mode)

@classmethod
async def open(cls, *args: Any, **kwargs: Any) -> Self:
store = cls(*args, **kwargs)
await store._open()
return store

async def _open(self) -> None:
if self._is_open:
raise ValueError("store is already open")
if not await self.empty():
if self.mode.update or self.mode.readonly:
pass
elif self.mode.overwrite:
await self.clear()
else:
raise FileExistsError("Store already exists")
self._is_open = True

async def _ensure_open(self) -> None:
if not self._is_open:
await self._open()

def __init__(self, mode: OpenMode = "r"):
if mode not in ("r", "r+", "w", "w-", "a"):
raise ValueError("mode must be one of 'r', 'r+', 'w', 'w-', 'a'")
self._mode = mode
@abstractmethod
async def empty(self) -> bool: ...

@abstractmethod
async def clear(self) -> None: ...

@property
def mode(self) -> OpenMode:
def mode(self) -> AccessMode:
"""Access mode of the store."""
return self._mode

@property
def writeable(self) -> bool:
"""Is the store writeable?"""
return self.mode in ("a", "w", "w-")

def _check_writable(self) -> None:
if not self.writeable:
if self.mode.readonly:
raise ValueError("store mode does not support writing")

@abstractmethod
Expand Down Expand Up @@ -173,8 +216,9 @@ def list_dir(self, prefix: str) -> AsyncGenerator[str, None]:
"""
...

def close(self) -> None: # noqa: B027
def close(self) -> None:
"""Close the store."""
self._is_open = False
pass


Expand Down
42 changes: 22 additions & 20 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from zarr.array import Array, AsyncArray
from zarr.buffer import NDArrayLike
from zarr.chunk_key_encodings import ChunkKeyEncoding
from zarr.common import JSON, ChunkCoords, MemoryOrder, OpenMode, ZarrFormat
from zarr.common import JSON, AccessModeLiteral, ChunkCoords, MemoryOrder, ZarrFormat
from zarr.group import AsyncGroup
from zarr.metadata import ArrayV2Metadata, ArrayV3Metadata
from zarr.store import (
Expand Down Expand Up @@ -158,7 +158,7 @@ async def load(
async def open(
*,
store: StoreLike | None = None,
mode: OpenMode | None = None, # type and value changed
mode: AccessModeLiteral | None = None, # type and value changed
zarr_version: ZarrFormat | None = None, # deprecated
zarr_format: ZarrFormat | None = None,
path: str | None = None,
Expand Down Expand Up @@ -189,15 +189,15 @@ async def open(
Return type depends on what exists in the given store.
"""
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
store_path = make_store_path(store, mode=mode)
store_path = await make_store_path(store, mode=mode)

if path is not None:
store_path = store_path / path

try:
return await open_array(store=store_path, zarr_format=zarr_format, **kwargs)
return await open_array(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)
except KeyError:
return await open_group(store=store_path, zarr_format=zarr_format, **kwargs)
return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs)


async def open_consolidated(*args: Any, **kwargs: Any) -> AsyncGroup:
Expand Down Expand Up @@ -267,7 +267,7 @@ async def save_array(
or _default_zarr_version()
)

store_path = make_store_path(store, mode="w")
store_path = await make_store_path(store, mode="w")
if path is not None:
store_path = store_path / path
new = await AsyncArray.create(
Expand Down Expand Up @@ -421,7 +421,7 @@ async def group(
or _default_zarr_version()
)

store_path = make_store_path(store)
store_path = await make_store_path(store)
if path is not None:
store_path = store_path / path

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

store_path = make_store_path(store, mode=mode)
store_path = await make_store_path(store, mode=mode)
if path is not None:
store_path = store_path / path

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

mode = cast(OpenMode, "r" if read_only else "w")
store_path = make_store_path(store, mode=mode)
mode = kwargs.pop("mode", cast(AccessModeLiteral, "r" if read_only else "w"))
store_path = await make_store_path(store, mode=mode)
if path is not None:
store_path = store_path / path

Expand Down Expand Up @@ -854,22 +854,24 @@ async def open_array(
The opened array.
"""

store_path = make_store_path(store)
store_path = await make_store_path(store)
if path is not None:
store_path = store_path / path

zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)

try:
return await AsyncArray.open(store_path, zarr_format=zarr_format)
except KeyError as e:
if store_path.store.writeable:
pass
else:
raise e

# if array was not found, create it
return await create(store=store, path=path, zarr_format=zarr_format, **kwargs)
except FileNotFoundError as e:
if store_path.store.mode.create:
return await create(
store=store_path,
path=path,
zarr_format=zarr_format,
overwrite=store_path.store.mode.overwrite,
**kwargs,
)
raise e


async def open_like(a: ArrayLike, path: str, **kwargs: Any) -> AsyncArray:
Expand Down
6 changes: 3 additions & 3 deletions src/zarr/api/synchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import zarr.api.asynchronous as async_api
from zarr.array import Array, AsyncArray
from zarr.buffer import NDArrayLike
from zarr.common import JSON, ChunkCoords, OpenMode, ZarrFormat
from zarr.common import JSON, AccessModeLiteral, ChunkCoords, ZarrFormat
from zarr.group import Group
from zarr.store import StoreLike
from zarr.sync import sync
Expand Down Expand Up @@ -36,7 +36,7 @@ def load(
def open(
*,
store: StoreLike | None = None,
mode: OpenMode | None = None, # type and value changed
mode: AccessModeLiteral | None = None, # type and value changed
zarr_version: ZarrFormat | None = None, # deprecated
zarr_format: ZarrFormat | None = None,
path: str | None = None,
Expand Down Expand Up @@ -161,7 +161,7 @@ def group(
def open_group(
*, # Note: this is a change from v2
store: StoreLike | None = None,
mode: OpenMode | None = None, # not used in async api
mode: AccessModeLiteral | None = None, # not used in async api
cache_attrs: bool | None = None, # default changed, not used in async api
synchronizer: Any = None, # not used in async api
path: str | None = None,
Expand Down
12 changes: 6 additions & 6 deletions src/zarr/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ async def create(
exists_ok: bool = False,
data: npt.ArrayLike | None = None,
) -> AsyncArray:
store_path = make_store_path(store)
store_path = await make_store_path(store)

if chunk_shape is None:
if chunks is None:
Expand Down Expand Up @@ -334,18 +334,18 @@ async def open(
store: StoreLike,
zarr_format: ZarrFormat | None = 3,
) -> AsyncArray:
store_path = make_store_path(store)
store_path = await make_store_path(store)

if zarr_format == 2:
zarray_bytes, zattrs_bytes = await gather(
(store_path / ZARRAY_JSON).get(), (store_path / ZATTRS_JSON).get()
)
if zarray_bytes is None:
raise KeyError(store_path) # filenotfounderror?
raise FileNotFoundError(store_path)
elif zarr_format == 3:
zarr_json_bytes = await (store_path / ZARR_JSON).get()
if zarr_json_bytes is None:
raise KeyError(store_path) # filenotfounderror?
raise FileNotFoundError(store_path)
elif zarr_format is None:
zarr_json_bytes, zarray_bytes, zattrs_bytes = await gather(
(store_path / ZARR_JSON).get(),
Expand All @@ -357,7 +357,7 @@ async def open(
# alternatively, we could warn and favor v3
raise ValueError("Both zarr.json and .zarray objects exist")
if zarr_json_bytes is None and zarray_bytes is None:
raise KeyError(store_path) # filenotfounderror?
raise FileNotFoundError(store_path)
# set zarr_format based on which keys were found
if zarr_json_bytes is not None:
zarr_format = 3
Expand Down Expand Up @@ -412,7 +412,7 @@ def attrs(self) -> dict[str, JSON]:

@property
def read_only(self) -> bool:
return bool(not self.store_path.store.writeable)
return self.store_path.store.mode.readonly

@property
def path(self) -> str:
Expand Down
2 changes: 1 addition & 1 deletion src/zarr/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
ZarrFormat = Literal[2, 3]
JSON = None | str | int | float | Enum | dict[str, "JSON"] | list["JSON"] | tuple["JSON", ...]
MemoryOrder = Literal["C", "F"]
OpenMode = Literal["r", "r+", "a", "w", "w-"]
AccessModeLiteral = Literal["r", "r+", "a", "w", "w-"]


def product(tup: ChunkCoords) -> int:
Expand Down
6 changes: 3 additions & 3 deletions src/zarr/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def create(
exists_ok: bool = False,
zarr_format: ZarrFormat = 3,
) -> AsyncGroup:
store_path = make_store_path(store)
store_path = await make_store_path(store)
if not exists_ok:
await ensure_no_existing_node(store_path, zarr_format=zarr_format)
attributes = attributes or {}
Expand All @@ -146,7 +146,7 @@ async def open(
store: StoreLike,
zarr_format: Literal[2, 3, None] = 3,
) -> AsyncGroup:
store_path = make_store_path(store)
store_path = await make_store_path(store)

if zarr_format == 2:
zgroup_bytes, zattrs_bytes = await asyncio.gather(
Expand All @@ -169,7 +169,7 @@ async def open(
# alternatively, we could warn and favor v3
raise ValueError("Both zarr.json and .zgroup objects exist")
if zarr_json_bytes is None and zgroup_bytes is None:
raise KeyError(store_path) # filenotfounderror?
raise FileNotFoundError(store_path)
# set zarr_format based on which keys were found
if zarr_json_bytes is not None:
zarr_format = 3
Expand Down
19 changes: 11 additions & 8 deletions src/zarr/store/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from pathlib import Path
from typing import Any, Literal

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


def make_store_path(store_like: StoreLike | None, *, mode: OpenMode | None = None) -> StorePath:
async def make_store_path(
store_like: StoreLike | None, *, mode: AccessModeLiteral | None = None
) -> StorePath:
if isinstance(store_like, StorePath):
if mode is not None:
assert mode == store_like.store.mode
assert AccessMode.from_literal(mode) == store_like.store.mode
return store_like
elif isinstance(store_like, Store):
if mode is not None:
assert mode == store_like.mode
assert AccessMode.from_literal(mode) == store_like.mode
await store_like._ensure_open()
return StorePath(store_like)
elif store_like is None:
if mode is None:
mode = "w" # exception to the default mode = 'r'
return StorePath(MemoryStore(mode=mode))
return StorePath(await MemoryStore.open(mode=mode))
elif isinstance(store_like, Path):
return StorePath(LocalStore(store_like, mode=mode or "r"))
return StorePath(await LocalStore.open(root=store_like, mode=mode or "r"))
elif isinstance(store_like, str):
return StorePath(LocalStore(Path(store_like), mode=mode or "r"))
return StorePath(await LocalStore.open(root=Path(store_like), mode=mode or "r"))
raise TypeError


Expand Down
Loading