Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changes/4455.misc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`zarr.create` and `zarr.api.asynchronous.create` now list `mode` and `data` in their signatures and docstrings. Both were accepted before without being declared. An unknown keyword now raises a `TypeError` from `create` before anything in the store is touched. Previously, with `mode="w"`, the existing data was deleted before the error was raised. `mode` is documented as a legacy spelling; `overwrite` is preferred.
26 changes: 23 additions & 3 deletions src/zarr/api/asynchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -922,7 +922,8 @@ async def create(
dimension_names: DimensionNamesLike = None,
storage_options: dict[str, Any] | None = None,
config: ArrayConfigLike | None = None,
**kwargs: Any,
mode: AccessModeLiteral | None = None,
data: npt.ArrayLike | None = None,
) -> AnyAsyncArray:
"""Create an array.

Expand Down Expand Up @@ -1042,6 +1043,26 @@ async def create(
config : ArrayConfigLike, optional
Runtime configuration of the array. If provided, will override the
default values from `zarr.config.array`.
mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Legacy way to control overwriting, kept for compatibility with Zarr-Python 2.
Prefer `overwrite`. The access mode used to open `store`; the default, `None`,
is `'a'`.

- `'a'` and `'r+'` create the array and fail if a node exists at `path`,
unless `overwrite` is `True`.
- `'w'` replaces anything stored under `path` (the whole store if `path` is
not set), even if `overwrite` is `False`. On a store that cannot delete
keys, `'w'` raises an error instead of replacing an existing node.
- `'w-'` fails if anything is stored under `path`, even if `overwrite` is
`True`.
- `'r'` always fails.

`mode` has no effect, and is not validated, if `store` is a `StorePath`.
data : array-like, optional
Values written into the new array after it is created. Unlike the `data`
parameter of `create_array`, it does not set the shape or data type of the
array. To create an array from existing data, use `create_array(data=...)`.
A Zarr array is not supported as `data`.

Returns
-------
Expand All @@ -1066,7 +1087,6 @@ async def create(
if write_empty_chunks is not None:
_warn_write_empty_chunks_kwarg()

mode = kwargs.pop("mode", None)
if mode is None:
mode = "a"
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
Expand Down Expand Up @@ -1102,7 +1122,7 @@ async def create(
dimension_names=dimension_names,
attributes=attributes,
config=config_parsed,
**kwargs,
data=data,
)


Expand Down
27 changes: 24 additions & 3 deletions src/zarr/api/synchronous.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,6 @@ def create_group(
)


# TODO: add type annotations for kwargs
def create(
shape: tuple[int, ...] | int,
*, # Note: this is a change from v2
Expand Down Expand Up @@ -666,7 +665,8 @@ def create(
dimension_names: DimensionNamesLike = None,
storage_options: dict[str, Any] | None = None,
config: ArrayConfigLike | None = None,
**kwargs: Any,
mode: AccessModeLiteral | None = None,
data: npt.ArrayLike | None = None,
) -> AnyArray:
"""Create an array.

Expand Down Expand Up @@ -786,6 +786,26 @@ def create(
config : ArrayConfigLike, optional
Runtime configuration of the array. If provided, will override the
default values from `zarr.config.array`.
mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
Legacy way to control overwriting, kept for compatibility with Zarr-Python 2.
Prefer `overwrite`. The access mode used to open `store`; the default, `None`,
is `'a'`.

- `'a'` and `'r+'` create the array and fail if a node exists at `path`,
unless `overwrite` is `True`.
- `'w'` replaces anything stored under `path` (the whole store if `path` is
not set), even if `overwrite` is `False`. On a store that cannot delete
keys, `'w'` raises an error instead of replacing an existing node.
- `'w-'` fails if anything is stored under `path`, even if `overwrite` is
`True`.
- `'r'` always fails.

`mode` has no effect, and is not validated, if `store` is a `StorePath`.
data : array-like, optional
Values written into the new array after it is created. Unlike the `data`
parameter of `create_array`, it does not set the shape or data type of the
array. To create an array from existing data, use `create_array(data=...)`.
A Zarr array is not supported as `data`.

Returns
-------
Expand Down Expand Up @@ -822,7 +842,8 @@ def create(
dimension_names=dimension_names,
storage_options=storage_options,
config=config,
**kwargs,
mode=mode,
data=data,
)
)
)
Expand Down
119 changes: 117 additions & 2 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
from collections.abc import Callable
from pathlib import Path

import numpy.typing as npt

from zarr.abc.store import Store
from zarr.core.common import JSON, MemoryOrder, ZarrFormat
from zarr.core.common import JSON, AccessModeLiteral, MemoryOrder, ZarrFormat
from zarr.types import AnyArray

import contextlib
Expand Down Expand Up @@ -43,9 +45,11 @@
save_array,
save_group,
)
from zarr.core.buffer import NDArrayLike
from zarr.core.buffer import NDArrayLike, default_buffer_prototype
from zarr.core.sync import sync
from zarr.errors import (
ArrayNotFoundError,
ContainsArrayError,
MetadataValidationError,
ZarrDeprecationWarning,
ZarrUserWarning,
Expand Down Expand Up @@ -84,6 +88,117 @@ def test_create(memory_store: Store) -> None:
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type]


def _create_async(**kwargs: Any) -> AnyArray:
return Array(sync(zarr.api.asynchronous.create(**kwargs)))


CREATE_FUNCS = pytest.mark.parametrize(
"create_func", [create, _create_async], ids=["sync", "async"]
)


def _write_existing_array(store: MemoryStore) -> None:
"""
Store an array with written chunks, plus a key that belongs to no Zarr node.
"""
old = create_array(store, shape=(4,), chunks=(2,), dtype="int8")
old[:] = [1, 2, 3, 4]
sync(store.set("junk", default_buffer_prototype().buffer.from_bytes(b"junk")))


@CREATE_FUNCS
@pytest.mark.parametrize(
("mode", "overwrite", "existing"),
[
(None, False, False),
("a", False, False),
("r+", False, False),
("w", False, False),
("w-", False, False),
("a", True, True),
("r+", True, True),
("w", False, True),
("w", True, True),
],
ids=lambda v: repr(v),
)
@pytest.mark.parametrize("data", [None, np.arange(3, dtype="int8")], ids=["no-data", "int8-data"])
def test_create_mode_data(
create_func: Callable[..., AnyArray],
mode: AccessModeLiteral | None,
overwrite: bool,
existing: bool,
data: npt.NDArray[Any] | None,
) -> None:
"""
`create` opens the store with `mode`, replaces what is stored under the path when
`mode` or `overwrite` says so, and writes `data` without taking its data type.
"""
store = MemoryStore()
if existing:
_write_existing_array(store)
reference = MemoryStore()
create(shape=(3,), chunks=(2,), store=reference, data=data)

z = create_func(shape=(3,), chunks=(2,), store=store, mode=mode, overwrite=overwrite, data=data)

assert sorted(store._store_dict) == sorted(reference._store_dict)
assert z.dtype == np.dtype("float64")
assert_array_equal(z[:], np.zeros(3) if data is None else data)


@pytest.mark.parametrize("mode", [None, "a", "r+"])
def test_create_existing_array_without_overwrite(mode: AccessModeLiteral | None) -> None:
"""
`create` refuses to replace an existing array unless `mode` or `overwrite` allows it.
"""
store = MemoryStore()
_write_existing_array(store)
with pytest.raises(ContainsArrayError):
create(shape=(3,), store=store, mode=mode)


@pytest.mark.parametrize("overwrite", [False, True])
def test_create_mode_w_minus_existing_data(overwrite: bool) -> None:
"""
`create` with mode `'w-'` refuses to write where data is stored, whatever `overwrite` says.
"""
store = MemoryStore()
_write_existing_array(store)
with pytest.raises(FileExistsError, match="mode 'w-'"):
create(shape=(3,), store=store, mode="w-", overwrite=overwrite)


def test_create_mode_r() -> None:
"""
`create` with mode `'r'` opens the store read-only, so creation fails.
"""
with pytest.raises(ValueError, match="read-only"):
create(shape=(3,), store=MemoryStore(), mode="r")


def test_create_invalid_mode() -> None:
"""
`create` rejects a mode that is not an access mode.
"""
with pytest.raises(ValueError, match="Invalid mode: x"):
create(shape=(3,), store=MemoryStore(), mode="x") # type: ignore[arg-type]


@CREATE_FUNCS
def test_create_unknown_keyword(create_func: Callable[..., AnyArray]) -> None:
"""
`create` rejects keyword arguments it does not declare, naming itself in the error,
before `mode="w"` deletes anything.
"""
store = MemoryStore()
_write_existing_array(store)
before = dict(store._store_dict)
with pytest.raises(TypeError, match=r"^create\(\) got an unexpected keyword argument 'typo'"):
create_func(shape=(3,), store=store, mode="w", typo=1)
assert store._store_dict == before


@pytest.mark.parametrize(
"func",
[
Expand Down
Loading