diff --git a/docs/guide/storage.rst b/docs/guide/storage.rst index 0019f993a2..69de796b3d 100644 --- a/docs/guide/storage.rst +++ b/docs/guide/storage.rst @@ -2,7 +2,7 @@ Storage ======= Zarr-Python supports multiple storage backends, including: local file systems, -Zip files, remote stores via ``fspec`` (S3, HTTP, etc.), and in-memory stores. In +Zip files, remote stores via ``fsspec`` (S3, HTTP, etc.), and in-memory stores. In Zarr-Python 3, stores must implement the abstract store API from :class:`zarr.abc.store.Store`. @@ -19,9 +19,9 @@ to Zarr's top level API will result in the store being created automatically. .. code-block:: python >>> import zarr - >>> zarr.open("data/foo/bar", mode="r") # implicitly creates a LocalStore + >>> zarr.open("data/foo/bar", mode="r") # implicitly creates a read-only LocalStore - >>> zarr.open("s3://foo/bar", mode="r") # implicitly creates a RemoteStore + >>> zarr.open("s3://foo/bar", mode="r") # implicitly creates a read-only RemoteStore >>> data = {} >>> zarr.open(data, mode="w") # implicitly creates a MemoryStore @@ -43,7 +43,7 @@ filesystem. .. code-block:: python >>> import zarr - >>> store = zarr.storage.LocalStore("data/foo/bar", mode="r") + >>> store = zarr.storage.LocalStore("data/foo/bar", read_only=True) >>> zarr.open(store=store) @@ -72,7 +72,7 @@ that implements the `AbstractFileSystem` API, .. code-block:: python >>> import zarr - >>> store = zarr.storage.RemoteStore.from_url("gs://foo/bar", mode="r") + >>> store = zarr.storage.RemoteStore.from_url("gs://foo/bar", read_only=True) >>> zarr.open(store=store) shape=(10, 20) dtype=float32> @@ -86,7 +86,7 @@ Zarr data (metadata and chunks) to a dictionary. >>> import zarr >>> data = {} - >>> store = zarr.storage.MemoryStore(data, mode="w") + >>> store = zarr.storage.MemoryStore(data) >>> zarr.open(store=store, shape=(2, )) diff --git a/src/zarr/abc/store.py b/src/zarr/abc/store.py index e7acce78da..fa3c7f3bdf 100644 --- a/src/zarr/abc/store.py +++ b/src/zarr/abc/store.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from asyncio import gather from itertools import starmap -from typing import TYPE_CHECKING, NamedTuple, Protocol, runtime_checkable +from typing import TYPE_CHECKING, Protocol, runtime_checkable if TYPE_CHECKING: from collections.abc import AsyncGenerator, AsyncIterator, Iterable @@ -11,64 +11,24 @@ from typing import Any, Self, TypeAlias from zarr.core.buffer import Buffer, BufferPrototype - from zarr.core.common import AccessModeLiteral, BytesLike + from zarr.core.common import BytesLike -__all__ = ["AccessMode", "ByteGetter", "ByteSetter", "Store", "set_or_delete"] +__all__ = ["ByteGetter", "ByteSetter", "Store", "set_or_delete"] ByteRangeRequest: TypeAlias = tuple[int | None, int | None] -class AccessMode(NamedTuple): - """Access mode flags.""" - - str: AccessModeLiteral - readonly: bool - overwrite: bool - create: bool - update: bool - - @classmethod - def from_literal(cls, mode: AccessModeLiteral) -> Self: - """ - Create an AccessMode instance from a literal. - - Parameters - ---------- - mode : AccessModeLiteral - One of 'r', 'r+', 'w', 'w-', 'a'. - - Returns - ------- - AccessMode - The created instance. - - Raises - ------ - ValueError - If mode is not one of 'r', 'r+', 'w', 'w-', 'a'. - """ - if mode in ("r", "r+", "a", "w", "w-"): - return cls( - str=mode, - 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): """ Abstract base class for Zarr stores. """ - _mode: AccessMode + _read_only: bool _is_open: bool - def __init__(self, *args: Any, mode: AccessModeLiteral = "r", **kwargs: Any) -> None: + def __init__(self, *, read_only: bool = False) -> None: self._is_open = False - self._mode = AccessMode.from_literal(mode) + self._read_only = read_only @classmethod async def open(cls, *args: Any, **kwargs: Any) -> Self: @@ -112,19 +72,9 @@ async def _open(self) -> None: ------ ValueError If the store is already open. - FileExistsError - If ``mode='w-'`` and the store already exists. - - Notes - ----- - * When ``mode='w'`` and the store already exists, it will be cleared. """ if self._is_open: raise ValueError("store is already open") - if self.mode.str == "w": - await self.clear() - elif self.mode.str == "w-" and not await self.empty(): - raise FileExistsError("Store already exists") self._is_open = True async def _ensure_open(self) -> None: @@ -132,61 +82,50 @@ async def _ensure_open(self) -> None: if not self._is_open: await self._open() - @abstractmethod - async def empty(self) -> bool: + async def is_empty(self, prefix: str) -> bool: """ - Check if the store is empty. + Check if the directory is empty. + + Parameters + ---------- + prefix : str + Prefix of keys to check. Returns ------- bool True if the store is empty, False otherwise. """ - ... + if not self.supports_listing: + raise NotImplementedError + if prefix != "" and not prefix.endswith("/"): + prefix += "/" + async for _ in self.list_prefix(prefix): + return False + return True - @abstractmethod async def clear(self) -> None: """ Clear the store. Remove all keys and values from the store. """ - ... - - @abstractmethod - def with_mode(self, mode: AccessModeLiteral) -> Self: - """ - Return a new store of the same type pointing to the same location with a new mode. - - The returned Store is not automatically opened. Call :meth:`Store.open` before - using. - - Parameters - ---------- - mode : AccessModeLiteral - The new mode to use. - - Returns - ------- - store - A new store of the same type with the new mode. - - Examples - -------- - >>> writer = zarr.store.MemoryStore(mode="w") - >>> reader = writer.with_mode("r") - """ - ... + if not self.supports_deletes: + raise NotImplementedError + if not self.supports_listing: + raise NotImplementedError + self._check_writable() + await self.delete_dir("") @property - def mode(self) -> AccessMode: - """Access mode of the store.""" - return self._mode + def read_only(self) -> bool: + """Is the store read-only?""" + return self._read_only def _check_writable(self) -> None: """Raise an exception if the store is not writable.""" - if self.mode.readonly: - raise ValueError("store mode does not support writing") + if self.read_only: + raise ValueError("store was opened in read-only mode and does not support writing") @abstractmethod def __eq__(self, value: object) -> bool: @@ -385,7 +324,7 @@ async def delete_dir(self, prefix: str) -> None: if not self.supports_listing: raise NotImplementedError self._check_writable() - if not prefix.endswith("/"): + if prefix != "" and not prefix.endswith("/"): prefix += "/" async for key in self.list_prefix(prefix): await self.delete(key) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 40f9b8d5f4..94ecfee4d6 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -8,7 +8,6 @@ import numpy as np import numpy.typing as npt -from zarr.abc.store import Store from zarr.core.array import Array, AsyncArray, get_array_metadata from zarr.core.buffer import NDArrayLike from zarr.core.common import ( @@ -24,7 +23,6 @@ from zarr.errors import NodeTypeValidationError from zarr.storage import ( StoreLike, - StorePath, make_store_path, ) @@ -67,6 +65,18 @@ ] +_READ_MODES: tuple[AccessModeLiteral, ...] = ("r", "r+", "a") +_CREATE_MODES: tuple[AccessModeLiteral, ...] = ("a", "w", "w-") +_OVERWRITE_MODES: tuple[AccessModeLiteral, ...] = ("a", "r+", "w") + + +def _infer_exists_ok(mode: AccessModeLiteral) -> bool: + """ + Check that an ``AccessModeLiteral`` is compatible with overwriting an existing Zarr node. + """ + return mode in _OVERWRITE_MODES + + def _get_shape_chunks(a: ArrayLike | Any) -> tuple[ChunkCoords | None, ChunkCoords | None]: """Helper function to get the shape and chunks from an array-like object""" shape = None @@ -252,7 +262,7 @@ async def load( async def open( *, store: StoreLike | None = None, - mode: AccessModeLiteral | None = None, # type and value changed + mode: AccessModeLiteral = "a", zarr_version: ZarrFormat | None = None, # deprecated zarr_format: ZarrFormat | None = None, path: str | None = None, @@ -290,7 +300,8 @@ async def open( store_path = await make_store_path(store, mode=mode, path=path, storage_options=storage_options) - if "shape" not in kwargs and mode in {"a", "w", "w-"}: + # TODO: the mode check below seems wrong! + if "shape" not in kwargs and mode in {"a", "r", "r+"}: try: metadata_dict = await get_array_metadata(store_path, zarr_format=zarr_format) # TODO: remove this cast when we fix typing for array metadata dicts @@ -300,17 +311,17 @@ async def open( is_v3_array = zarr_format == 3 and _metadata_dict.get("node_type") == "array" if is_v3_array or zarr_format == 2: return AsyncArray(store_path=store_path, metadata=_metadata_dict) - except (AssertionError, FileNotFoundError): + except (AssertionError, FileNotFoundError, NodeTypeValidationError): pass return await open_group(store=store_path, zarr_format=zarr_format, mode=mode, **kwargs) 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, NodeTypeValidationError): # KeyError for a missing key # NodeTypeValidationError for failing to parse node metadata as an array when it's # actually a group - 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( @@ -396,18 +407,20 @@ async def save_array( if not isinstance(arr, NDArrayLike): raise TypeError("arr argument must be numpy or other NDArrayLike array") - mode = kwargs.pop("mode", None) + mode = kwargs.pop("mode", "a") store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) if np.isscalar(arr): arr = np.array(arr) shape = arr.shape chunks = getattr(arr, "chunks", None) # for array-likes with chunks attribute + exists_ok = kwargs.pop("exists_ok", None) or _infer_exists_ok(mode) new = await AsyncArray.create( store_path, zarr_format=zarr_format, shape=shape, dtype=arr.dtype, chunks=chunks, + exists_ok=exists_ok, **kwargs, ) await new.setitem(slice(None), arr) @@ -441,6 +454,9 @@ async def save_group( **kwargs NumPy arrays with data to save. """ + + store_path = await make_store_path(store, path=path, mode="w", storage_options=storage_options) + zarr_format = ( _handle_zarr_version_or_format( zarr_version=zarr_version, @@ -465,7 +481,7 @@ async def save_group( _path = f"{path}/arr_{i}" if path is not None else f"arr_{i}" aws.append( save_array( - store, + store_path, arr, zarr_format=zarr_format, path=_path, @@ -473,12 +489,7 @@ async def save_group( ) ) for k, arr in kwargs.items(): - _path = f"{path}/{k}" if path is not None else k - aws.append( - save_array( - store, arr, zarr_format=zarr_format, path=_path, storage_options=storage_options - ) - ) + aws.append(save_array(store_path, arr, zarr_format=zarr_format, path=k)) await asyncio.gather(*aws) @@ -588,8 +599,11 @@ async def group( zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format) - mode = None if isinstance(store, Store) else cast(AccessModeLiteral, "a") - + mode: AccessModeLiteral + if overwrite: + mode = "w" + else: + mode = "r+" store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) if chunk_store is not None: @@ -607,9 +621,10 @@ async def group( try: return await AsyncGroup.open(store=store_path, zarr_format=zarr_format) except (KeyError, FileNotFoundError): + _zarr_format = zarr_format or _default_zarr_version() return await AsyncGroup.from_store( store=store_path, - zarr_format=zarr_format or _default_zarr_version(), + zarr_format=_zarr_format, exists_ok=overwrite, attributes=attributes, ) @@ -618,7 +633,7 @@ async def group( async def open_group( store: StoreLike | None = None, *, # Note: this is a change from v2 - mode: AccessModeLiteral | None = None, + mode: AccessModeLiteral = "a", cache_attrs: bool | None = None, # not used, default changed synchronizer: Any = None, # not used path: str | None = None, @@ -709,16 +724,22 @@ async def open_group( attributes = {} try: - return await AsyncGroup.open( - store_path, zarr_format=zarr_format, use_consolidated=use_consolidated - ) + if mode in _READ_MODES: + return await AsyncGroup.open( + store_path, zarr_format=zarr_format, use_consolidated=use_consolidated + ) except (KeyError, FileNotFoundError): + pass + if mode in _CREATE_MODES: + exists_ok = _infer_exists_ok(mode) + _zarr_format = zarr_format or _default_zarr_version() return await AsyncGroup.from_store( store_path, - zarr_format=zarr_format or _default_zarr_version(), - exists_ok=True, + zarr_format=_zarr_format, + exists_ok=exists_ok, attributes=attributes, ) + raise FileNotFoundError(f"Unable to find group: {store_path}") async def create( @@ -861,6 +882,8 @@ async def create( warnings.warn("cache_attrs is not yet implemented", RuntimeWarning, stacklevel=2) if object_codec is not None: warnings.warn("object_codec is not yet implemented", RuntimeWarning, stacklevel=2) + if read_only is not None: + warnings.warn("read_only is not yet implemented", RuntimeWarning, stacklevel=2) if dimension_separator is not None: if zarr_format == 3: raise ValueError( @@ -878,11 +901,9 @@ async def create( warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2) mode = kwargs.pop("mode", None) - if mode is None and not isinstance(store, Store | StorePath): + if mode is None: mode = "a" - store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) - return await AsyncArray.create( store_path, shape=shape, @@ -890,7 +911,7 @@ async def create( dtype=dtype, compressor=compressor, fill_value=fill_value, - exists_ok=overwrite, # TODO: name change + exists_ok=overwrite, filters=filters, dimension_separator=dimension_separator, zarr_format=zarr_format, @@ -1060,7 +1081,7 @@ async def open_array( If using an fsspec URL to create the store, these will be passed to the backend implementation. Ignored otherwise. **kwargs - Any keyword arguments to pass to the array constructor. + Any keyword arguments to pass to ``create``. Returns ------- @@ -1069,18 +1090,20 @@ async def open_array( """ mode = kwargs.pop("mode", None) - store_path = await make_store_path(store, path=path, mode=mode) + store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options) 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 FileNotFoundError: - if store_path.store.mode.create: + if not store_path.read_only: + exists_ok = _infer_exists_ok(mode) + _zarr_format = zarr_format or _default_zarr_version() return await create( store=store_path, - zarr_format=zarr_format or _default_zarr_version(), - overwrite=store_path.store.mode.overwrite, + zarr_format=_zarr_format, + overwrite=exists_ok, **kwargs, ) raise diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index 9dcd6fe2d5..9616c41355 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -68,7 +68,7 @@ def load( def open( store: StoreLike | None = None, *, - mode: AccessModeLiteral | None = None, # type and value changed + mode: AccessModeLiteral = "a", zarr_version: ZarrFormat | None = None, # deprecated zarr_format: ZarrFormat | None = None, path: str | None = None, @@ -199,8 +199,8 @@ def group( @_deprecate_positional_args def open_group( store: StoreLike | None = None, - *, # Note: this is a change from v2 - mode: AccessModeLiteral | None = None, # not used in async api + *, + mode: AccessModeLiteral = "a", cache_attrs: bool | None = None, # default changed, not used in async api synchronizer: Any = None, # not used in async api path: str | None = None, diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 20446abe7d..eeff8378ae 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -796,7 +796,7 @@ def read_only(self) -> bool: True if the array is read-only """ # Backwards compatibility for 2.x - return self.store_path.store.mode.readonly + return self.store_path.read_only @property def path(self) -> str: @@ -1384,7 +1384,7 @@ def _info(self, extra: dict[str, int] | None = None) -> Any: _zarr_format=self.metadata.zarr_format, _shape=self.shape, _order=self.order, - _read_only=self.store_path.store.mode.readonly, + _read_only=self.read_only, _store_type=type(self.store_path.store).__name__, _count_bytes=self.dtype.itemsize * self.size, **kwargs, diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index 160306049d..86d5d4794a 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -866,7 +866,7 @@ def _info( return GroupInfo( _name=self.store_path.path, - _read_only=self.store_path.store.mode.readonly, + _read_only=self.read_only, _store_type=type(self.store_path.store).__name__, _zarr_format=self.metadata.zarr_format, # maybe do a typeddict @@ -880,7 +880,7 @@ def store(self) -> Store: @property def read_only(self) -> bool: # Backwards compatibility for 2.x - return self.store_path.store.mode.readonly + return self.store_path.read_only @property def synchronizer(self) -> None: diff --git a/src/zarr/storage/common.py b/src/zarr/storage/common.py index 1317ab7983..0485a33576 100644 --- a/src/zarr/storage/common.py +++ b/src/zarr/storage/common.py @@ -6,7 +6,7 @@ from zarr.abc.store import ByteRangeRequest, Store from zarr.core.buffer import Buffer, default_buffer_prototype -from zarr.core.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, ZarrFormat +from zarr.core.common import ZARR_JSON, ZARRAY_JSON, ZGROUP_JSON, AccessModeLiteral, ZarrFormat from zarr.errors import ContainsArrayAndGroupError, ContainsArrayError, ContainsGroupError from zarr.storage._utils import normalize_path from zarr.storage.local import LocalStore @@ -16,7 +16,6 @@ if TYPE_CHECKING: from zarr.core.buffer import BufferPrototype - from zarr.core.common import AccessModeLiteral def _dereference_path(root: str, path: str) -> str: @@ -46,6 +45,62 @@ def __init__(self, store: Store, path: str = "") -> None: self.store = store self.path = path + @property + def read_only(self) -> bool: + return self.store.read_only + + @classmethod + async def open( + cls, store: Store, path: str, mode: AccessModeLiteral | None = None + ) -> StorePath: + """ + Open StorePath based on the provided mode. + + * If the mode is 'w-' and the StorePath contains keys, raise a FileExistsError. + * If the mode is 'w', delete all keys nested within the StorePath + * If the mode is 'a', 'r', or 'r+', do nothing + + Parameters + ---------- + mode : AccessModeLiteral + The mode to use when initializing the store path. + + Raises + ------ + FileExistsError + If the mode is 'w-' and the store path already exists. + """ + + await store._ensure_open() + self = cls(store, path) + + # fastpath if mode is None + if mode is None: + return self + + if store.read_only and mode != "r": + raise ValueError(f"Store is read-only but mode is '{mode}'") + + match mode: + case "w-": + if not await self.is_empty(): + msg = ( + f"{self} is not empty, but `mode` is set to 'w-'." + "Either remove the existing objects in storage," + "or set `mode` to a value that handles pre-existing objects" + "in storage, like `a` or `w`." + ) + raise FileExistsError(msg) + case "w": + await self.delete_dir() + case "a" | "r" | "r+": + # No init action + pass + case _: + raise ValueError(f"Invalid mode: {mode}") + + return self + async def get( self, prototype: BufferPrototype | None = None, @@ -132,6 +187,17 @@ async def exists(self) -> bool: """ return await self.store.exists(self.path) + async def is_empty(self) -> bool: + """ + Check if any keys exist in the store with the given prefix. + + Returns + ------- + bool + True if no keys exist in the store with the given prefix, False otherwise. + """ + return await self.store.is_empty(self.path) + def __truediv__(self, other: str) -> StorePath: """Combine this store path with another path""" return self.__class__(self.store, _dereference_path(self.path, other)) @@ -203,7 +269,7 @@ async def make_store_path( path : str | None, optional The path to use when creating the `StorePath` object. If None, the default path is the empty string. - mode : AccessModeLiteral | None, optional + mode : StoreAccessMode | None, optional The mode to use when creating the `StorePath` object. If None, the default mode is 'r'. storage_options : dict[str, Any] | None, optional @@ -225,45 +291,36 @@ async def make_store_path( used_storage_options = False path_normalized = normalize_path(path) if isinstance(store_like, StorePath): - if mode is not None and mode != store_like.store.mode.str: - _store = store_like.store.with_mode(mode) - await _store._ensure_open() - store_like = StorePath(_store, path=store_like.path) result = store_like / path_normalized - elif isinstance(store_like, Store): - if mode is not None and mode != store_like.mode.str: - store_like = store_like.with_mode(mode) - await store_like._ensure_open() - result = StorePath(store_like, path=path_normalized) - elif store_like is None: - # mode = "w" is an exception to the default mode = 'r' - result = StorePath(await MemoryStore.open(mode=mode or "w"), path=path_normalized) - elif isinstance(store_like, Path): - result = StorePath( - await LocalStore.open(root=store_like, mode=mode or "r"), path=path_normalized - ) - elif isinstance(store_like, str): - storage_options = storage_options or {} - - if _is_fsspec_uri(store_like): - used_storage_options = True - result = StorePath( - RemoteStore.from_url(store_like, storage_options=storage_options, mode=mode or "r"), - path=path_normalized, - ) - else: - result = StorePath( - await LocalStore.open(root=Path(store_like), mode=mode or "r"), path=path_normalized - ) - elif isinstance(store_like, dict): - # We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings. - # By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate. - result = StorePath( - await MemoryStore.open(store_dict=store_like, mode=mode or "r"), path=path_normalized - ) else: - msg = f"Unsupported type for store_like: '{type(store_like).__name__}'" # type: ignore[unreachable] - raise TypeError(msg) + assert mode in (None, "r", "r+", "a", "w", "w-") + # if mode 'r' was provided, we'll open any new stores as read-only + _read_only = mode == "r" + if isinstance(store_like, Store): + store = store_like + elif store_like is None: + store = await MemoryStore.open(read_only=_read_only) + elif isinstance(store_like, Path): + store = await LocalStore.open(root=store_like, read_only=_read_only) + elif isinstance(store_like, str): + storage_options = storage_options or {} + + if _is_fsspec_uri(store_like): + used_storage_options = True + store = RemoteStore.from_url( + store_like, storage_options=storage_options, read_only=_read_only + ) + else: + store = await LocalStore.open(root=Path(store_like), read_only=_read_only) + elif isinstance(store_like, dict): + # We deliberate only consider dict[str, Buffer] here, and not arbitrary mutable mappings. + # By only allowing dictionaries, which are in-memory, we know that MemoryStore appropriate. + store = await MemoryStore.open(store_dict=store_like, read_only=_read_only) + else: + msg = f"Unsupported type for store_like: '{type(store_like).__name__}'" # type: ignore[unreachable] + raise TypeError(msg) + + result = await StorePath.open(store, path=path_normalized, mode=mode) if storage_options and not used_storage_options: msg = "'storage_options' was provided but unused. 'storage_options' is only used for fsspec filesystem stores." diff --git a/src/zarr/storage/local.py b/src/zarr/storage/local.py index a5d0f6429c..f6e94ae479 100644 --- a/src/zarr/storage/local.py +++ b/src/zarr/storage/local.py @@ -2,10 +2,9 @@ import asyncio import io -import os import shutil from pathlib import Path -from typing import TYPE_CHECKING, Self +from typing import TYPE_CHECKING from zarr.abc.store import ByteRangeRequest, Store from zarr.core.buffer import Buffer @@ -15,7 +14,6 @@ from collections.abc import AsyncIterator, Iterable from zarr.core.buffer import BufferPrototype - from zarr.core.common import AccessModeLiteral def _get( @@ -74,8 +72,8 @@ class LocalStore(Store): ---------- root : str or Path Directory to use as root of store. - mode : str - Mode in which to open the store. Either 'r', 'r+', 'a', 'w', 'w-'. + read_only : bool + Whether the store is read-only Attributes ---------- @@ -93,8 +91,8 @@ class LocalStore(Store): root: Path - def __init__(self, root: Path | str, *, mode: AccessModeLiteral = "r") -> None: - super().__init__(mode=mode) + def __init__(self, root: Path | str, *, read_only: bool = False) -> None: + super().__init__(read_only=read_only) if isinstance(root, str): root = Path(root) if not isinstance(root, Path): @@ -104,7 +102,7 @@ def __init__(self, root: Path | str, *, mode: AccessModeLiteral = "r") -> None: self.root = root async def _open(self) -> None: - if not self.mode.readonly: + if not self.read_only: self.root.mkdir(parents=True, exist_ok=True) return await super()._open() @@ -114,23 +112,6 @@ async def clear(self) -> None: shutil.rmtree(self.root) self.root.mkdir() - async def empty(self) -> bool: - # docstring inherited - try: - with os.scandir(self.root) as it: - for entry in it: - if entry.is_file(): - # stop once a file is found - return False - except FileNotFoundError: - return True - else: - return True - - def with_mode(self, mode: AccessModeLiteral) -> Self: - # docstring inherited - return type(self)(root=self.root, mode=mode) - def __str__(self) -> str: return f"file://{self.root.as_posix()}" diff --git a/src/zarr/storage/logging.py b/src/zarr/storage/logging.py index e79b7fa44c..3b11ddbba7 100644 --- a/src/zarr/storage/logging.py +++ b/src/zarr/storage/logging.py @@ -5,15 +5,14 @@ import time from collections import defaultdict from contextlib import contextmanager -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any -from zarr.abc.store import AccessMode, ByteRangeRequest, Store +from zarr.abc.store import ByteRangeRequest, Store if TYPE_CHECKING: from collections.abc import AsyncIterator, Generator, Iterable from zarr.core.buffer import Buffer, BufferPrototype - from zarr.core.common import AccessModeLiteral class LoggingStore(Store): @@ -114,9 +113,9 @@ def supports_listing(self) -> bool: return self._store.supports_listing @property - def _mode(self) -> AccessMode: # type: ignore[override] + def read_only(self) -> bool: with self.log(): - return self._store._mode + return self._store.read_only @property def _is_open(self) -> bool: @@ -136,10 +135,10 @@ async def _ensure_open(self) -> None: with self.log(): return await self._store._ensure_open() - async def empty(self) -> bool: + async def is_empty(self, prefix: str = "") -> bool: # docstring inherited with self.log(): - return await self._store.empty() + return await self._store.is_empty(prefix=prefix) async def clear(self) -> None: # docstring inherited @@ -226,12 +225,3 @@ async def delete_dir(self, prefix: str) -> None: # docstring inherited with self.log(prefix): await self._store.delete_dir(prefix=prefix) - - def with_mode(self, mode: AccessModeLiteral) -> Self: - # docstring inherited - with self.log(mode): - return type(self)( - self._store.with_mode(mode), - log_level=self.log_level, - log_handler=self.log_handler, - ) diff --git a/src/zarr/storage/memory.py b/src/zarr/storage/memory.py index 85abe1697f..74d7758863 100644 --- a/src/zarr/storage/memory.py +++ b/src/zarr/storage/memory.py @@ -12,7 +12,6 @@ from collections.abc import AsyncIterator, Iterable, MutableMapping from zarr.core.buffer import BufferPrototype - from zarr.core.common import AccessModeLiteral logger = getLogger(__name__) @@ -26,8 +25,8 @@ class MemoryStore(Store): ---------- store_dict : dict Initial data - mode : str - Access mode + read_only : bool + Whether the store is read-only Attributes ---------- @@ -48,25 +47,17 @@ def __init__( self, store_dict: MutableMapping[str, Buffer] | None = None, *, - mode: AccessModeLiteral = "r", + read_only: bool = False, ) -> None: - super().__init__(mode=mode) + super().__init__(read_only=read_only) if store_dict is None: store_dict = {} self._store_dict = store_dict - async def empty(self) -> bool: - # docstring inherited - return not self._store_dict - async def clear(self) -> None: # docstring inherited self._store_dict.clear() - def with_mode(self, mode: AccessModeLiteral) -> Self: - # docstring inherited - return type(self)(store_dict=self._store_dict, mode=mode) - def __str__(self) -> str: return f"memory://{id(self._store_dict)}" @@ -77,7 +68,7 @@ def __eq__(self, other: object) -> bool: return ( isinstance(other, type(self)) and self._store_dict == other._store_dict - and self.mode == other.mode + and self.read_only == other.read_only ) async def get( @@ -194,6 +185,8 @@ class GpuMemoryStore(MemoryStore): store_dict : MutableMapping, optional A mutable mapping with string keys and :class:`zarr.core.buffer.gpu.Buffer` values. + read_only : bool + Whether to open the store in read-only mode. """ _store_dict: MutableMapping[str, gpu.Buffer] # type: ignore[assignment] @@ -202,9 +195,9 @@ def __init__( self, store_dict: MutableMapping[str, gpu.Buffer] | None = None, *, - mode: AccessModeLiteral = "r", + read_only: bool = False, ) -> None: - super().__init__(store_dict=store_dict, mode=mode) # type: ignore[arg-type] + super().__init__(store_dict=store_dict, read_only=read_only) # type: ignore[arg-type] def __str__(self) -> str: return f"gpumemory://{id(self._store_dict)}" diff --git a/src/zarr/storage/remote.py b/src/zarr/storage/remote.py index 9628069776..c08963bfaa 100644 --- a/src/zarr/storage/remote.py +++ b/src/zarr/storage/remote.py @@ -1,7 +1,7 @@ from __future__ import annotations import warnings -from typing import TYPE_CHECKING, Any, Self +from typing import TYPE_CHECKING, Any from zarr.abc.store import ByteRangeRequest, Store from zarr.storage.common import _dereference_path @@ -12,7 +12,7 @@ from fsspec.asyn import AsyncFileSystem from zarr.core.buffer import Buffer, BufferPrototype - from zarr.core.common import AccessModeLiteral, BytesLike + from zarr.core.common import BytesLike ALLOWED_EXCEPTIONS: tuple[type[Exception], ...] = ( @@ -30,8 +30,8 @@ class RemoteStore(Store): ---------- fs : AsyncFileSystem The Async FSSpec filesystem to use with this store. - mode : AccessModeLiteral - The access mode to use. + read_only : bool + Whether the store is read-only path : str The root path of the store. This should be a relative path and must not include the filesystem scheme. @@ -77,11 +77,11 @@ class RemoteStore(Store): def __init__( self, fs: AsyncFileSystem, - mode: AccessModeLiteral = "r", + read_only: bool = False, path: str = "/", allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> None: - super().__init__(mode=mode) + super().__init__(read_only=read_only) self.fs = fs self.path = path self.allowed_exceptions = allowed_exceptions @@ -102,7 +102,7 @@ def __init__( def from_upath( cls, upath: Any, - mode: AccessModeLiteral = "r", + read_only: bool = False, allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> RemoteStore: """ @@ -112,8 +112,8 @@ def from_upath( ---------- upath : UPath The upath to the root of the store. - mode : str, optional - The mode of the store. Defaults to "r". + read_only : bool + Whether the store is read-only, defaults to False. allowed_exceptions : tuple, optional The exceptions that are allowed to be raised when accessing the store. Defaults to ALLOWED_EXCEPTIONS. @@ -125,7 +125,7 @@ def from_upath( return cls( fs=upath.fs, path=upath.path.rstrip("/"), - mode=mode, + read_only=read_only, allowed_exceptions=allowed_exceptions, ) @@ -134,7 +134,7 @@ def from_url( cls, url: str, storage_options: dict[str, Any] | None = None, - mode: AccessModeLiteral = "r", + read_only: bool = False, allowed_exceptions: tuple[type[Exception], ...] = ALLOWED_EXCEPTIONS, ) -> RemoteStore: """ @@ -146,8 +146,8 @@ def from_url( The URL to the root of the store. storage_options : dict, optional The options to pass to fsspec when creating the filesystem. - mode : str, optional - The mode of the store. Defaults to "r". + read_only : bool + Whether the store is read-only, defaults to False. allowed_exceptions : tuple, optional The exceptions that are allowed to be raised when accessing the store. Defaults to ALLOWED_EXCEPTIONS. @@ -173,7 +173,7 @@ def from_url( # `not path.startswith("http")` is a special case for the http filesystem (¯\_(ツ)_/¯) path = fs._strip_protocol(path) - return cls(fs=fs, path=path, mode=mode, allowed_exceptions=allowed_exceptions) + return cls(fs=fs, path=path, read_only=read_only, allowed_exceptions=allowed_exceptions) async def clear(self) -> None: # docstring inherited @@ -184,25 +184,6 @@ async def clear(self) -> None: except FileNotFoundError: pass - async def empty(self) -> bool: - # docstring inherited - - # TODO: it would be nice if we didn't have to list all keys here - # it should be possible to stop after the first key is discovered - try: - return not await self.fs._ls(self.path) - except FileNotFoundError: - return True - - def with_mode(self, mode: AccessModeLiteral) -> Self: - # docstring inherited - return type(self)( - fs=self.fs, - mode=mode, - path=self.path, - allowed_exceptions=self.allowed_exceptions, - ) - def __repr__(self) -> str: return f"" @@ -210,7 +191,7 @@ def __eq__(self, other: object) -> bool: return ( isinstance(other, type(self)) and self.path == other.path - and self.mode == other.mode + and self.read_only == other.read_only and self.fs == other.fs ) diff --git a/src/zarr/storage/zip.py b/src/zarr/storage/zip.py index 968f81cdec..755588fb7a 100644 --- a/src/zarr/storage/zip.py +++ b/src/zarr/storage/zip.py @@ -5,7 +5,7 @@ import time import zipfile from pathlib import Path -from typing import TYPE_CHECKING, Any, Literal, Self +from typing import TYPE_CHECKING, Any, Literal from zarr.abc.store import ByteRangeRequest, Store from zarr.core.buffer import Buffer, BufferPrototype @@ -65,10 +65,14 @@ def __init__( path: Path | str, *, mode: ZipStoreAccessModeLiteral = "r", + read_only: bool | None = None, compression: int = zipfile.ZIP_STORED, allowZip64: bool = True, ) -> None: - super().__init__(mode=mode) + if read_only is None: + read_only = mode == "r" + + super().__init__(read_only=read_only) if isinstance(path, str): path = Path(path) @@ -121,15 +125,6 @@ async def clear(self) -> None: self.path, mode="w", compression=self.compression, allowZip64=self.allowZip64 ) - async def empty(self) -> bool: - # docstring inherited - with self._lock: - return not self._zf.namelist() - - def with_mode(self, mode: ZipStoreAccessModeLiteral) -> Self: # type: ignore[override] - # docstring inherited - raise NotImplementedError("ZipStore cannot be reopened with a new mode.") - def __str__(self) -> str: return f"zip://{self.path}" @@ -217,10 +212,19 @@ async def set_if_not_exists(self, key: str, value: Buffer) -> None: if key not in members: self._set(key, value) + async def delete_dir(self, prefix: str) -> None: + # only raise NotImplementedError if any keys are found + self._check_writable() + if not prefix.endswith("/"): + prefix += "/" + async for _ in self.list_prefix(prefix): + raise NotImplementedError + async def delete(self, key: str) -> None: # docstring inherited # we choose to only raise NotImplementedError here if the key exists # this allows the array/group APIs to avoid the overhead of existence checks + self._check_writable() if await self.exists(key): raise NotImplementedError diff --git a/src/zarr/testing/store.py b/src/zarr/testing/store.py index 3aece0f4a9..b544bf87e2 100644 --- a/src/zarr/testing/store.py +++ b/src/zarr/testing/store.py @@ -1,11 +1,10 @@ import pickle -from typing import Any, Generic, TypeVar, cast +from typing import Any, Generic, TypeVar import pytest -from zarr.abc.store import AccessMode, Store +from zarr.abc.store import Store from zarr.core.buffer import Buffer, default_buffer_prototype -from zarr.core.common import AccessModeLiteral from zarr.core.sync import _collect_aiterator from zarr.storage._utils import _normalize_interval_index from zarr.testing.utils import assert_bytes_equal @@ -40,7 +39,7 @@ async def get(self, store: S, key: str) -> Buffer: @pytest.fixture def store_kwargs(self) -> dict[str, Any]: - return {"mode": "r+"} + return {"read_only": False} @pytest.fixture async def store(self, store_kwargs: dict[str, Any]) -> Store: @@ -63,27 +62,25 @@ def test_serializable_store(self, store: S) -> None: foo = pickle.dumps(store) assert pickle.loads(foo) == store - def test_store_mode(self, store: S, store_kwargs: dict[str, Any]) -> None: - assert store.mode == AccessMode.from_literal("r+") - assert not store.mode.readonly + def test_store_read_only(self, store: S) -> None: + assert not store.read_only with pytest.raises(AttributeError): - store.mode = AccessMode.from_literal("w") # type: ignore[misc] + store.read_only = False # type: ignore[misc] - @pytest.mark.parametrize("mode", ["r", "r+", "a", "w", "w-"]) - async def test_store_open_mode( - self, store_kwargs: dict[str, Any], mode: AccessModeLiteral + @pytest.mark.parametrize("read_only", [True, False]) + async def test_store_open_read_only( + self, store_kwargs: dict[str, Any], read_only: bool ) -> None: - store_kwargs["mode"] = mode + store_kwargs["read_only"] = read_only store = await self.store_cls.open(**store_kwargs) assert store._is_open - assert store.mode == AccessMode.from_literal(mode) + assert store.read_only == read_only - async def test_not_writable_store_raises(self, store_kwargs: dict[str, Any]) -> None: - kwargs = {**store_kwargs, "mode": "r"} + async def test_read_only_store_raises(self, store_kwargs: dict[str, Any]) -> None: + kwargs = {**store_kwargs, "read_only": True} store = await self.store_cls.open(**kwargs) - assert store.mode == AccessMode.from_literal("r") - assert store.mode.readonly + assert store.read_only # set with pytest.raises(ValueError): @@ -149,7 +146,7 @@ async def test_set(self, store: S, key: str, data: bytes) -> None: """ Ensure that data can be written to the store using the store.set method. """ - assert not store.mode.readonly + assert not store.read_only data_buf = self.buffer_cls.from_bytes(data) await store.set(key, data_buf) observed = await self.get(store, key) @@ -233,19 +230,23 @@ async def test_delete_dir(self, store: S) -> None: assert not await store.exists("foo/zarr.json") assert not await store.exists("foo/c/0") - async def test_empty(self, store: S) -> None: - assert await store.empty() + async def test_is_empty(self, store: S) -> None: + assert await store.is_empty("") await self.set( - store, "key", self.buffer_cls.from_bytes(bytes("something", encoding="utf-8")) + store, "foo/bar", self.buffer_cls.from_bytes(bytes("something", encoding="utf-8")) ) - assert not await store.empty() + assert not await store.is_empty("") + assert await store.is_empty("fo") + assert not await store.is_empty("foo/") + assert not await store.is_empty("foo") + assert await store.is_empty("spam/") async def test_clear(self, store: S) -> None: await self.set( store, "key", self.buffer_cls.from_bytes(bytes("something", encoding="utf-8")) ) await store.clear() - assert await store.empty() + assert await store.is_empty("") async def test_list(self, store: S) -> None: assert await _collect_aiterator(store.list()) == () @@ -302,41 +303,6 @@ async def test_list_dir(self, store: S) -> None: keys_observed = await _collect_aiterator(store.list_dir(root + "/")) assert sorted(keys_expected) == sorted(keys_observed) - async def test_with_mode(self, store: S) -> None: - data = b"0000" - await self.set(store, "key", self.buffer_cls.from_bytes(data)) - assert (await self.get(store, "key")).to_bytes() == data - - for mode in ["r", "a"]: - mode = cast(AccessModeLiteral, mode) - clone = store.with_mode(mode) - # await store.close() - await clone._ensure_open() - assert clone.mode == AccessMode.from_literal(mode) - assert isinstance(clone, type(store)) - - # earlier writes are visible - result = await clone.get("key", default_buffer_prototype()) - assert result is not None - assert result.to_bytes() == data - - # writes to original after with_mode is visible - await self.set(store, "key-2", self.buffer_cls.from_bytes(data)) - result = await clone.get("key-2", default_buffer_prototype()) - assert result is not None - assert result.to_bytes() == data - - if mode == "a": - # writes to clone is visible in the original - await clone.set("key-3", self.buffer_cls.from_bytes(data)) - result = await clone.get("key-3", default_buffer_prototype()) - assert result is not None - assert result.to_bytes() == data - - else: - with pytest.raises(ValueError, match="store mode"): - await clone.set("key-3", self.buffer_cls.from_bytes(data)) - async def test_set_if_not_exists(self, store: S) -> None: key = "k" data_buf = self.buffer_cls.from_bytes(b"0000") diff --git a/src/zarr/testing/strategies.py b/src/zarr/testing/strategies.py index 9d254e7481..aed5b82e57 100644 --- a/src/zarr/testing/strategies.py +++ b/src/zarr/testing/strategies.py @@ -6,8 +6,8 @@ from hypothesis import given, settings # noqa: F401 from hypothesis.strategies import SearchStrategy +import zarr from zarr.core.array import Array -from zarr.core.group import Group from zarr.core.sync import sync from zarr.storage import MemoryStore, StoreLike @@ -138,7 +138,7 @@ def arrays( expected_attrs = {} if attributes is None else attributes array_path = path + ("/" if not path.endswith("/") else "") + name - root = Group.from_store(store, zarr_format=zarr_format) + root = zarr.open_group(store, mode="w", zarr_format=zarr_format) a = root.create_array( array_path, diff --git a/tests/conftest.py b/tests/conftest.py index 8c66406c9b..35f31d39b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,11 +28,11 @@ async def parse_store( store: Literal["local", "memory", "remote", "zip"], path: str ) -> LocalStore | MemoryStore | RemoteStore | ZipStore: if store == "local": - return await LocalStore.open(path, mode="w") + return await LocalStore.open(path) if store == "memory": - return await MemoryStore.open(mode="w") + return await MemoryStore.open() if store == "remote": - return await RemoteStore.open(url=path, mode="w") + return await RemoteStore.open(url=path) if store == "zip": return await ZipStore.open(path + "/zarr.zip", mode="w") raise AssertionError @@ -46,23 +46,23 @@ def path_type(request: pytest.FixtureRequest) -> Any: # todo: harmonize this with local_store fixture @pytest.fixture async def store_path(tmpdir: LEGACY_PATH) -> StorePath: - store = await LocalStore.open(str(tmpdir), mode="w") + store = await LocalStore.open(str(tmpdir)) return StorePath(store) @pytest.fixture async def local_store(tmpdir: LEGACY_PATH) -> LocalStore: - return await LocalStore.open(str(tmpdir), mode="w") + return await LocalStore.open(str(tmpdir)) @pytest.fixture async def remote_store(url: str) -> RemoteStore: - return await RemoteStore.open(url, mode="w") + return await RemoteStore.open(url) @pytest.fixture async def memory_store() -> MemoryStore: - return await MemoryStore.open(mode="w") + return await MemoryStore.open() @pytest.fixture diff --git a/tests/test_api.py b/tests/test_api.py index 7e6ef4ce9b..fa3d80014d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -72,15 +72,15 @@ async def test_open_array(memory_store: MemoryStore) -> None: # open array, overwrite # store._store_dict = {} - store = MemoryStore(mode="w") + store = MemoryStore() z = open(store=store, shape=200) assert isinstance(z, Array) assert z.shape == (200,) # open array, read-only store_cls = type(store) - ro_store = await store_cls.open(store_dict=store._store_dict, mode="r") - z = open(store=ro_store) + ro_store = await store_cls.open(store_dict=store._store_dict, read_only=True) + z = open(store=ro_store, mode="r") assert isinstance(z, Array) assert z.shape == (200,) assert z.read_only @@ -106,10 +106,10 @@ async def test_open_group(memory_store: MemoryStore) -> None: # open group, read-only store_cls = type(store) - ro_store = await store_cls.open(store_dict=store._store_dict, mode="r") - g = open_group(store=ro_store) + ro_store = await store_cls.open(store_dict=store._store_dict, read_only=True) + g = open_group(store=ro_store, mode="r") assert isinstance(g, Group) - # assert g.read_only + assert g.read_only @pytest.mark.parametrize("zarr_format", [None, 2, 3]) @@ -965,13 +965,13 @@ def test_tree() -> None: def test_open_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() with pytest.warns(FutureWarning, match="pass"): open(store, "w", shape=(1,)) def test_save_array_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="zarr_version is deprecated", category=DeprecationWarning @@ -987,20 +987,20 @@ def test_save_array_positional_args_deprecated() -> None: def test_group_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() with pytest.warns(FutureWarning, match="pass"): group(store, True) def test_open_group_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() with pytest.warns(FutureWarning, match="pass"): open_group(store, "w") def test_open_falls_back_to_open_group() -> None: # https://github.com/zarr-developers/zarr-python/issues/2309 - store = MemoryStore(mode="w") + store = MemoryStore() zarr.open_group(store, attributes={"key": "value"}) group = zarr.open(store) @@ -1010,7 +1010,7 @@ def test_open_falls_back_to_open_group() -> None: async def test_open_falls_back_to_open_group_async() -> None: # https://github.com/zarr-developers/zarr-python/issues/2309 - store = MemoryStore(mode="w") + store = MemoryStore() await zarr.api.asynchronous.open_group(store, attributes={"key": "value"}) group = await zarr.api.asynchronous.open(store=store) diff --git a/tests/test_array.py b/tests/test_array.py index 5f73f2f44e..3948896186 100644 --- a/tests/test_array.py +++ b/tests/test_array.py @@ -196,13 +196,13 @@ def test_array_v3_fill_value(store: MemoryStore, fill_value: int, dtype_str: str def test_create_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() with pytest.warns(FutureWarning, match="Pass"): Array.create(store, (2, 2), dtype="f8") def test_selection_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() arr = Array.create(store, shape=(2, 2), dtype="f8") with pytest.warns(FutureWarning, match="Pass out"): @@ -315,7 +315,7 @@ def test_nchunks(test_cls: type[Array] | type[AsyncArray[Any]], nchunks: int) -> """ Test that nchunks returns the number of chunks defined for the array. """ - store = MemoryStore({}, mode="w") + store = MemoryStore() shape = 100 arr = Array.create(store, shape=(shape,), chunks=(ceildiv(shape, nchunks),), dtype="i4") expected = nchunks @@ -331,7 +331,7 @@ async def test_nchunks_initialized(test_cls: type[Array] | type[AsyncArray[Any]] """ Test that nchunks_initialized accurately returns the number of stored chunks. """ - store = MemoryStore({}, mode="w") + store = MemoryStore() arr = Array.create(store, shape=(100,), chunks=(10,), dtype="i4") # write chunks one at a time @@ -359,7 +359,7 @@ async def test_chunks_initialized() -> None: """ Test that chunks_initialized accurately returns the keys of stored chunks. """ - store = MemoryStore({}, mode="w") + store = MemoryStore() arr = Array.create(store, shape=(100,), chunks=(10,), dtype="i4") chunks_accumulated = tuple( @@ -373,34 +373,32 @@ async def test_chunks_initialized() -> None: def test_default_fill_values() -> None: - a = Array.create(MemoryStore({}, mode="w"), shape=5, chunk_shape=5, dtype=" None: with pytest.raises(ValueError, match="At least one ArrayBytesCodec is required."): - Array.create(MemoryStore({}, mode="w"), shape=5, chunk_shape=5, dtype=" None: @pytest.mark.parametrize("zarr_format", [2, 3]) def test_update_attrs(zarr_format: int) -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/2328 - store = MemoryStore({}, mode="w") + store = MemoryStore() arr = Array.create(store=store, shape=5, chunk_shape=5, dtype="f8", zarr_format=zarr_format) arr.attrs["foo"] = "bar" assert arr.attrs["foo"] == "bar" @@ -672,7 +670,7 @@ def test_array_create_order( ], ) async def test_special_complex_fill_values_roundtrip(fill_value: Any, expected: list[Any]) -> None: - store = MemoryStore({}, mode="w") + store = MemoryStore() Array.create(store=store, shape=(1,), dtype=np.complex64, fill_value=fill_value) content = await store.get("zarr.json", prototype=default_buffer_prototype()) assert content is not None diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 12097eb2bc..b26db5df89 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -4,7 +4,7 @@ def test_put() -> None: - store = zarr.storage.MemoryStore({}, mode="w") + store = zarr.storage.MemoryStore() attrs = zarr.core.attributes.Attributes( zarr.Group.from_store(store, attributes={"a": 1, "b": 2}) ) @@ -14,7 +14,7 @@ def test_put() -> None: def test_asdict() -> None: - store = zarr.storage.MemoryStore({}, mode="w") + store = zarr.storage.MemoryStore() attrs = zarr.core.attributes.Attributes( zarr.Group.from_store(store, attributes={"a": 1, "b": 2}) ) diff --git a/tests/test_buffer.py b/tests/test_buffer.py index 60816d764e..7a275516c6 100644 --- a/tests/test_buffer.py +++ b/tests/test_buffer.py @@ -48,7 +48,7 @@ async def test_async_array_prototype() -> None: expect = np.zeros((9, 9), dtype="uint16", order="F") a = await AsyncArray.create( - StorePath(StoreExpectingTestBuffer(mode="w")) / "test_async_array_prototype", + StorePath(StoreExpectingTestBuffer()) / "test_async_array_prototype", shape=expect.shape, chunk_shape=(5, 5), dtype=expect.dtype, @@ -77,7 +77,7 @@ async def test_async_array_gpu_prototype() -> None: expect = cp.zeros((9, 9), dtype="uint16", order="F") a = await AsyncArray.create( - StorePath(MemoryStore(mode="w")) / "test_async_array_gpu_prototype", + StorePath(MemoryStore()) / "test_async_array_gpu_prototype", shape=expect.shape, chunk_shape=(5, 5), dtype=expect.dtype, @@ -99,7 +99,7 @@ async def test_async_array_gpu_prototype() -> None: async def test_codecs_use_of_prototype() -> None: expect = np.zeros((10, 10), dtype="uint16", order="F") a = await AsyncArray.create( - StorePath(StoreExpectingTestBuffer(mode="w")) / "test_codecs_use_of_prototype", + StorePath(StoreExpectingTestBuffer()) / "test_codecs_use_of_prototype", shape=expect.shape, chunk_shape=(5, 5), dtype=expect.dtype, @@ -134,7 +134,7 @@ async def test_codecs_use_of_prototype() -> None: async def test_codecs_use_of_gpu_prototype() -> None: expect = cp.zeros((10, 10), dtype="uint16", order="F") a = await AsyncArray.create( - StorePath(MemoryStore(mode="w")) / "test_codecs_use_of_gpu_prototype", + StorePath(MemoryStore()) / "test_codecs_use_of_gpu_prototype", shape=expect.shape, chunk_shape=(5, 5), dtype=expect.dtype, diff --git a/tests/test_config.py b/tests/test_config.py index ddabffb467..2e919a0add 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -201,7 +201,7 @@ def test_config_buffer_implementation() -> None: # has default value assert fully_qualified_name(get_buffer_class()) == config.defaults[0]["buffer"] - arr = zeros(shape=(100), store=StoreExpectingTestBuffer(mode="w")) + arr = zeros(shape=(100), store=StoreExpectingTestBuffer()) # AssertionError of StoreExpectingTestBuffer when not using my buffer with pytest.raises(AssertionError): @@ -219,7 +219,7 @@ def test_config_buffer_implementation() -> None: data2d = np.arange(1000).reshape(100, 10) arr_sharding = zeros( shape=(100, 10), - store=StoreExpectingTestBuffer(mode="w"), + store=StoreExpectingTestBuffer(), codecs=[ShardingCodec(chunk_shape=(10, 10))], ) arr_sharding[:] = data2d @@ -227,7 +227,7 @@ def test_config_buffer_implementation() -> None: arr_Crc32c = zeros( shape=(100, 10), - store=StoreExpectingTestBuffer(mode="w"), + store=StoreExpectingTestBuffer(), codecs=[BytesCodec(), Crc32cCodec()], ) arr_Crc32c[:] = data2d diff --git a/tests/test_group.py b/tests/test_group.py index 6e185097fd..afa290207d 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1196,12 +1196,16 @@ async def test_members_name(store: Store, consolidate: bool, zarr_format: ZarrFo async def test_open_mutable_mapping(): - group = await zarr.api.asynchronous.open_group(store={}, mode="w") + group = await zarr.api.asynchronous.open_group( + store={}, + ) assert isinstance(group.store_path.store, MemoryStore) def test_open_mutable_mapping_sync(): - group = zarr.open_group(store={}, mode="w") + group = zarr.open_group( + store={}, + ) assert isinstance(group.store_path.store, MemoryStore) @@ -1347,7 +1351,7 @@ def test_from_dict_extra_fields(self): class TestInfo: def test_info(self): - store = zarr.storage.MemoryStore(mode="w") + store = zarr.storage.MemoryStore() A = zarr.group(store=store, path="A") B = A.create_group(name="B") @@ -1379,7 +1383,7 @@ def test_info(self): def test_update_attrs() -> None: # regression test for https://github.com/zarr-developers/zarr-python/issues/2328 root = Group.from_store( - MemoryStore({}, mode="w"), + MemoryStore(), ) root.attrs["foo"] = "bar" assert root.attrs["foo"] == "bar" diff --git a/tests/test_indexing.py b/tests/test_indexing.py index 2c51f3da3a..6f80705d9d 100644 --- a/tests/test_indexing.py +++ b/tests/test_indexing.py @@ -38,7 +38,7 @@ @pytest.fixture async def store() -> AsyncGenerator[StorePath]: - return StorePath(await MemoryStore.open(mode="w")) + return StorePath(await MemoryStore.open()) def zarr_array_from_numpy_array( @@ -62,7 +62,7 @@ class CountingDict(MemoryStore): @classmethod async def open(cls) -> CountingDict: - store = await super().open(mode="w") + store = await super().open() store.counter = Counter() return store diff --git a/tests/test_metadata/test_consolidated.py b/tests/test_metadata/test_consolidated.py index d9143d09d6..85e32f5d68 100644 --- a/tests/test_metadata/test_consolidated.py +++ b/tests/test_metadata/test_consolidated.py @@ -281,7 +281,7 @@ def test_consolidated_sync(self, memory_store): async def test_not_writable_raises(self, memory_store: zarr.storage.MemoryStore) -> None: await group(store=memory_store, attributes={"foo": "bar"}) - read_store = zarr.storage.MemoryStore(store_dict=memory_store._store_dict) + read_store = zarr.storage.MemoryStore(store_dict=memory_store._store_dict, read_only=True) with pytest.raises(ValueError, match="does not support writing"): await consolidate_metadata(read_store) @@ -457,7 +457,7 @@ def test_to_dict_empty(self): @pytest.mark.parametrize("zarr_format", [2, 3]) async def test_open_consolidated_raises_async(self, zarr_format: ZarrFormat): - store = zarr.storage.MemoryStore(mode="w") + store = zarr.storage.MemoryStore() await AsyncGroup.from_store(store, zarr_format=zarr_format) with pytest.raises(ValueError): await zarr.api.asynchronous.open_consolidated(store, zarr_format=zarr_format) @@ -466,7 +466,7 @@ async def test_open_consolidated_raises_async(self, zarr_format: ZarrFormat): await zarr.api.asynchronous.open_consolidated(store, zarr_format=None) async def test_consolidated_metadata_v2(self): - store = zarr.storage.MemoryStore(mode="w") + store = zarr.storage.MemoryStore() g = await AsyncGroup.from_store(store, attributes={"key": "root"}, zarr_format=2) await g.create_array(name="a", shape=(1,), attributes={"key": "a"}) g1 = await g.create_group(name="g1", attributes={"key": "g1"}) diff --git a/tests/test_metadata/test_v2.py b/tests/test_metadata/test_v2.py index 089d5c98e1..003aef331f 100644 --- a/tests/test_metadata/test_v2.py +++ b/tests/test_metadata/test_v2.py @@ -141,7 +141,7 @@ async def v2_consolidated_metadata( "zarr_consolidated_format": 1, } store_dict = {} - store = zarr.storage.MemoryStore(store_dict=store_dict, mode="a") + store = zarr.storage.MemoryStore(store_dict=store_dict) await store.set( ".zattrs", cpu.Buffer.from_bytes(json.dumps({"Conventions": "COARDS"}).encode()) ) diff --git a/tests/test_store/test_core.py b/tests/test_store/test_core.py index 38292e23c9..4d3f305e53 100644 --- a/tests/test_store/test_core.py +++ b/tests/test_store/test_core.py @@ -1,11 +1,11 @@ import tempfile from pathlib import Path -from typing import Literal import pytest from _pytest.compat import LEGACY_PATH from upath import UPath +from zarr.core.common import AccessModeLiteral from zarr.storage._utils import normalize_path from zarr.storage.common import StoreLike, StorePath, make_store_path from zarr.storage.local import LocalStore @@ -24,13 +24,13 @@ async def test_make_store_path_none(path: str) -> None: @pytest.mark.parametrize("path", [None, "", "bar"]) -@pytest.mark.parametrize("store_type", [str, Path, LocalStore]) -@pytest.mark.parametrize("mode", ["r", "w", "a"]) +@pytest.mark.parametrize("store_type", [str, Path]) +@pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_local( tmpdir: LEGACY_PATH, store_type: type[str] | type[Path] | type[LocalStore], path: str, - mode: Literal["r", "w", "a"], + mode: AccessModeLiteral, ) -> None: """ Test the various ways of invoking make_store_path that create a LocalStore @@ -40,26 +40,27 @@ async def test_make_store_path_local( assert isinstance(store_path.store, LocalStore) assert Path(store_path.store.root) == Path(tmpdir) assert store_path.path == normalize_path(path) - assert store_path.store.mode.str == mode + assert store_path.read_only == (mode == "r") @pytest.mark.parametrize("path", [None, "", "bar"]) -@pytest.mark.parametrize("mode", ["r", "w", "a"]) +@pytest.mark.parametrize("mode", ["r", "w"]) async def test_make_store_path_store_path( - tmpdir: LEGACY_PATH, path: str, mode: Literal["r", "w", "a"] + tmpdir: LEGACY_PATH, path: str, mode: AccessModeLiteral ) -> None: """ Test invoking make_store_path when the input is another store_path. In particular we want to ensure that a new path is handled correctly. """ - store_like = StorePath(LocalStore(str(tmpdir)), path="root") + ro = mode == "r" + store_like = await StorePath.open(LocalStore(str(tmpdir), read_only=ro), path="root", mode=mode) store_path = await make_store_path(store_like, path=path, mode=mode) assert isinstance(store_path.store, LocalStore) assert Path(store_path.store.root) == Path(tmpdir) path_normalized = normalize_path(path) assert store_path.path == (store_like / path_normalized).path - assert store_path.store.mode.str == mode + assert store_path.read_only == ro async def test_make_store_path_invalid() -> None: @@ -79,10 +80,10 @@ async def test_make_store_path_fsspec(monkeypatch) -> None: "store_like", [ None, - str(tempfile.TemporaryDirectory()), + tempfile.TemporaryDirectory().name, Path(tempfile.TemporaryDirectory().name), - StorePath(store=MemoryStore(store_dict={}, mode="w"), path="/"), - MemoryStore(store_dict={}, mode="w"), + StorePath(store=MemoryStore(store_dict={}), path="/"), + MemoryStore(store_dict={}), {}, ], ) diff --git a/tests/test_store/test_local.py b/tests/test_store/test_local.py index 239364a06d..c614d32c26 100644 --- a/tests/test_store/test_local.py +++ b/tests/test_store/test_local.py @@ -28,7 +28,7 @@ async def set(self, store: LocalStore, key: str, value: Buffer) -> None: @pytest.fixture def store_kwargs(self, tmpdir) -> dict[str, str]: - return {"root": str(tmpdir), "mode": "r+"} + return {"root": str(tmpdir)} def test_store_repr(self, store: LocalStore) -> None: assert str(store) == f"file://{store.root.as_posix()}" @@ -43,13 +43,13 @@ def test_store_supports_listing(self, store: LocalStore) -> None: assert store.supports_listing async def test_empty_with_empty_subdir(self, store: LocalStore) -> None: - assert await store.empty() + assert await store.is_empty("") (store.root / "foo/bar").mkdir(parents=True) - assert await store.empty() + assert await store.is_empty("") def test_creates_new_directory(self, tmp_path: pathlib.Path): target = tmp_path.joinpath("a", "b", "c") assert not target.exists() - store = self.store_cls(root=target, mode="w") + store = self.store_cls(root=target) zarr.group(store=store) diff --git a/tests/test_store/test_logging.py b/tests/test_store/test_logging.py index 50db5c1c5b..aed1f06fa2 100644 --- a/tests/test_store/test_logging.py +++ b/tests/test_store/test_logging.py @@ -54,10 +54,3 @@ async def test_logging_store_counter(store: Store) -> None: else: assert wrapped.counter["get"] == 1 assert wrapped.counter["delete_dir"] == 0 - - -async def test_with_mode(): - wrapped = LoggingStore(store=zarr.storage.MemoryStore(mode="w"), log_level="INFO") - new = wrapped.with_mode(mode="r") - assert new.mode.str == "r" - assert new.log_level == "INFO" diff --git a/tests/test_store/test_memory.py b/tests/test_store/test_memory.py index bcd9fc4448..56ccb4d3be 100644 --- a/tests/test_store/test_memory.py +++ b/tests/test_store/test_memory.py @@ -22,7 +22,7 @@ async def get(self, store: MemoryStore, key: str) -> Buffer: def store_kwargs( self, request: pytest.FixtureRequest ) -> dict[str, str | None | dict[str, Buffer]]: - kwargs = {"store_dict": None, "mode": "r+"} + kwargs = {"store_dict": None} if request.param is True: kwargs["store_dict"] = {} return kwargs @@ -62,7 +62,7 @@ async def get(self, store: MemoryStore, key: str) -> Buffer: def store_kwargs( self, request: pytest.FixtureRequest ) -> dict[str, str | None | dict[str, Buffer]]: - kwargs = {"store_dict": None, "mode": "r+"} + kwargs = {"store_dict": None} if request.param is True: kwargs["store_dict"] = {} return kwargs diff --git a/tests/test_store/test_remote.py b/tests/test_store/test_remote.py index 2ad1fd787b..aee620796c 100644 --- a/tests/test_store/test_remote.py +++ b/tests/test_store/test_remote.py @@ -87,7 +87,6 @@ def s3(s3_base: None) -> Generator[s3fs.S3FileSystem, None, None]: async def test_basic() -> None: store = RemoteStore.from_url( f"s3://{test_bucket_name}/foo/spam/", - mode="w", storage_options={"endpoint_url": endpoint_url, "anon": False}, ) assert store.fs.asynchronous @@ -113,7 +112,7 @@ def store_kwargs(self, request) -> dict[str, str | bool]: fs, path = fsspec.url_to_fs( f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=True ) - return {"fs": fs, "path": path, "mode": "r+"} + return {"fs": fs, "path": path} @pytest.fixture def store(self, store_kwargs: dict[str, str | bool]) -> RemoteStore: @@ -206,13 +205,12 @@ def test_init_warns_if_fs_asynchronous_is_false(self) -> None: fs, path = fsspec.url_to_fs( f"s3://{test_bucket_name}", endpoint_url=endpoint_url, anon=False, asynchronous=False ) - store_kwargs = {"fs": fs, "path": path, "mode": "r+"} + store_kwargs = {"fs": fs, "path": path} with pytest.warns(UserWarning, match=r".* was not created with `asynchronous=True`.*"): self.store_cls(**store_kwargs) async def test_empty_nonexistent_path(self, store_kwargs) -> None: # regression test for https://github.com/zarr-developers/zarr-python/pull/2343 - store_kwargs["mode"] = "w-" store_kwargs["path"] += "/abc" store = await self.store_cls.open(**store_kwargs) - assert await store.empty() + assert await store.is_empty("") diff --git a/tests/test_store/test_stateful_hierarchy.py b/tests/test_store/test_stateful_hierarchy.py index 40c8f44992..844e1227da 100644 --- a/tests/test_store/test_stateful_hierarchy.py +++ b/tests/test_store/test_stateful_hierarchy.py @@ -43,7 +43,7 @@ def __init__(self, store) -> None: self.store = store - self.model = MemoryStore(mode="w") + self.model = MemoryStore() zarr.group(store=self.model) # Track state of the hierarchy, these should contain fully qualified paths diff --git a/tests/test_store/test_stateful_store.py b/tests/test_store/test_stateful_store.py index 9ac3bbc3f6..751c1ac746 100644 --- a/tests/test_store/test_stateful_store.py +++ b/tests/test_store/test_stateful_store.py @@ -14,7 +14,7 @@ from hypothesis.strategies import DataObject import zarr -from zarr.abc.store import AccessMode, Store +from zarr.abc.store import Store from zarr.core.buffer import BufferPrototype, cpu, default_buffer_prototype from zarr.storage import LocalStore, ZipStore from zarr.testing.strategies import key_ranges @@ -35,8 +35,8 @@ def __init__(self, store: Store) -> None: self.store = store @property - def mode(self) -> AccessMode: - return self.store.mode + def read_only(self) -> bool: + return self.store.read_only def set(self, key: str, data_buffer: zarr.core.buffer.Buffer) -> None: return self._sync(self.store.set(key, data_buffer)) @@ -55,8 +55,8 @@ def get_partial_values( def delete(self, path: str) -> None: return self._sync(self.store.delete(path)) - def empty(self) -> bool: - return self._sync(self.store.empty()) + def is_empty(self, prefix: str) -> bool: + return self._sync(self.store.is_empty(prefix=prefix)) def clear(self) -> None: return self._sync(self.store.clear()) @@ -117,7 +117,7 @@ def init_store(self): @rule(key=zarr_keys, data=st.binary(min_size=0, max_size=MAX_BINARY_SIZE)) def set(self, key: str, data: DataObject) -> None: note(f"(set) Setting {key!r} with {data}") - assert not self.store.mode.readonly + assert not self.store.read_only data_buf = cpu.Buffer.from_bytes(data) self.store.set(key, data_buf) self.model[key] = data_buf @@ -179,23 +179,23 @@ def delete(self, data: DataObject) -> None: @rule() def clear(self) -> None: - assert not self.store.mode.readonly + assert not self.store.read_only note("(clear)") self.store.clear() self.model.clear() - assert self.store.empty() + assert self.store.is_empty("") assert len(self.model.keys()) == len(list(self.store.list())) == 0 @rule() # Local store can be non-empty when there are subdirectories but no files @precondition(lambda self: not isinstance(self.store.store, LocalStore)) - def empty(self) -> None: - note("(empty)") + def is_empty(self) -> None: + note("(is_empty)") # make sure they either both are or both aren't empty (same state) - assert self.store.empty() == (not self.model) + assert self.store.is_empty("") == (not self.model) @rule(key=zarr_keys) def exists(self, key: str) -> None: @@ -228,10 +228,10 @@ def check_zarr_keys(self) -> None: keys = list(self.store.list()) if not keys: - assert self.store.empty() is True + assert self.store.is_empty("") is True else: - assert self.store.empty() is False + assert self.store.is_empty("") is False for key in keys: assert self.store.exists(key) is True diff --git a/tests/test_store/test_zip.py b/tests/test_store/test_zip.py index 8f73989319..df22b76e1e 100644 --- a/tests/test_store/test_zip.py +++ b/tests/test_store/test_zip.py @@ -2,13 +2,13 @@ import os import tempfile +import zipfile from typing import TYPE_CHECKING import numpy as np import pytest import zarr -from zarr.abc.store import AccessMode from zarr.core.buffer import Buffer, cpu, default_buffer_prototype from zarr.storage.zip import ZipStore from zarr.testing.store import StoreTests @@ -25,8 +25,9 @@ class TestZipStore(StoreTests[ZipStore, cpu.Buffer]): def store_kwargs(self, request) -> dict[str, str | bool]: fd, temp_path = tempfile.mkstemp() os.close(fd) + os.unlink(temp_path) - return {"path": temp_path, "mode": "w"} + return {"path": temp_path, "mode": "w", "read_only": False} async def get(self, store: ZipStore, key: str) -> Buffer: return store._get(key, prototype=default_buffer_prototype()) @@ -34,19 +35,18 @@ async def get(self, store: ZipStore, key: str) -> Buffer: async def set(self, store: ZipStore, key: str, value: Buffer) -> None: return store._set(key, value) - def test_store_mode(self, store: ZipStore, store_kwargs: dict[str, Any]) -> None: - assert store.mode == AccessMode.from_literal(store_kwargs["mode"]) - assert not store.mode.readonly + def test_store_read_only(self, store: ZipStore, store_kwargs: dict[str, Any]) -> None: + assert not store.read_only - async def test_not_writable_store_raises(self, store_kwargs: dict[str, Any]) -> None: + async def test_read_only_store_raises(self, store_kwargs: dict[str, Any]) -> None: # we need to create the zipfile in write mode before switching to read mode store = await self.store_cls.open(**store_kwargs) store.close() - kwargs = {**store_kwargs, "mode": "r"} + kwargs = {**store_kwargs, "mode": "a", "read_only": True} store = await self.store_cls.open(**kwargs) - assert store.mode == AccessMode.from_literal("r") - assert store.mode.readonly + assert store._zmode == "a" + assert store.read_only # set with pytest.raises(ValueError): @@ -65,7 +65,7 @@ def test_store_supports_listing(self, store: ZipStore) -> None: assert store.supports_listing def test_api_integration(self, store: ZipStore) -> None: - root = zarr.open_group(store=store) + root = zarr.open_group(store=store, mode="a") data = np.arange(10000, dtype=np.uint16).reshape(100, 100) z = root.create_array( @@ -93,10 +93,21 @@ def test_api_integration(self, store: ZipStore) -> None: store.close() - async def test_with_mode(self, store: ZipStore) -> None: - with pytest.raises(NotImplementedError, match="new mode"): - await super().test_with_mode(store) - - @pytest.mark.parametrize("mode", ["a", "w"]) - async def test_store_open_mode(self, store_kwargs: dict[str, Any], mode: str) -> None: - await super().test_store_open_mode(store_kwargs, mode) + @pytest.mark.parametrize("read_only", [True, False]) + async def test_store_open_read_only( + self, store_kwargs: dict[str, Any], read_only: bool + ) -> None: + if read_only == "r": + # create an empty zipfile + with zipfile.ZipFile(store_kwargs["path"], mode="w"): + pass + + await super().test_store_open_read_only(store_kwargs, read_only) + + @pytest.mark.parametrize(("zip_mode", "read_only"), [("w", False), ("a", False), ("x", False)]) + async def test_zip_open_mode_translation( + self, store_kwargs: dict[str, Any], zip_mode: str, read_only: bool + ) -> None: + kws = {**store_kwargs, "mode": zip_mode} + store = await self.store_cls.open(**kws) + assert store.read_only == read_only diff --git a/tests/test_sync.py b/tests/test_sync.py index bff3837e2f..02b3b594fd 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -142,7 +142,7 @@ def bar(self) -> list[int]: def test_open_positional_args_deprecate(): - store = MemoryStore({}, mode="w") + store = MemoryStore() with pytest.warns(FutureWarning, match="pass"): zarr.open(store, "w", shape=(1,)) diff --git a/tests/test_v2.py b/tests/test_v2.py index 3dd17848fb..94aa513ce8 100644 --- a/tests/test_v2.py +++ b/tests/test_v2.py @@ -16,7 +16,7 @@ @pytest.fixture async def store() -> Iterator[StorePath]: - return StorePath(await MemoryStore.open(mode="w")) + return StorePath(await MemoryStore.open()) def test_simple(store: StorePath) -> None: @@ -63,7 +63,7 @@ def test_implicit_fill_value(store: StorePath, dtype: str, fill_value: Any) -> N def test_codec_pipeline() -> None: # https://github.com/zarr-developers/zarr-python/issues/2243 - store = MemoryStore(mode="w") + store = MemoryStore() array = zarr.create( store=store, shape=(1,), @@ -80,7 +80,7 @@ def test_codec_pipeline() -> None: @pytest.mark.parametrize("dtype", ["|S", "|V"]) async def test_v2_encode_decode(dtype): - store = zarr.storage.MemoryStore(mode="w") + store = zarr.storage.MemoryStore() g = zarr.group(store=store, zarr_format=2) g.create_array( name="foo",