Skip to content

Commit 083adfc

Browse files
authored
feat(api): declare mode and data on create (#4455)
* feat(api): declare mode and data on create `create` accepted `mode` and `data` only through `**kwargs`: `mode` was popped for opening the store and `data` reached `AsyncArray._create`. Both are now named, documented parameters of the sync and async `create`, and `**kwargs` is gone, so an unknown keyword raises a TypeError from `create` itself. Assisted-by: ClaudeCode:claude-opus-5-5 * chore: number the changelog fragment for #4455 Assisted-by: ClaudeCode:claude-opus-5-5 * fix(api): document mode as legacy and pin create's mode and data behavior Adversarial review of #4455 found the new docstrings overpromised: `data` is not `array[...] = data` for a zarr Array, `'r'` does not always open the store read-only, and the `mode`/`overwrite` interaction was undocumented. `mode` is now documented as the legacy spelling with `overwrite` preferred, each mode's outcome is stated, and `data` points to `create_array(data=...)`. The tests now cover the async `create`, the default mode, the `mode`/`overwrite` combinations, that `'w'` removes old chunks and stray keys, invalid modes, and that an unknown keyword leaves the store untouched. The fragment moves to misc. Assisted-by: ClaudeCode:claude-opus-5-5
1 parent 9677081 commit 083adfc

4 files changed

Lines changed: 165 additions & 8 deletions

File tree

‎changes/4455.misc.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

‎src/zarr/api/asynchronous.py‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -922,7 +922,8 @@ async def create(
922922
dimension_names: DimensionNamesLike = None,
923923
storage_options: dict[str, Any] | None = None,
924924
config: ArrayConfigLike | None = None,
925-
**kwargs: Any,
925+
mode: AccessModeLiteral | None = None,
926+
data: npt.ArrayLike | None = None,
926927
) -> AnyAsyncArray:
927928
"""Create an array.
928929
@@ -1042,6 +1043,26 @@ async def create(
10421043
config : ArrayConfigLike, optional
10431044
Runtime configuration of the array. If provided, will override the
10441045
default values from `zarr.config.array`.
1046+
mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
1047+
Legacy way to control overwriting, kept for compatibility with Zarr-Python 2.
1048+
Prefer `overwrite`. The access mode used to open `store`; the default, `None`,
1049+
is `'a'`.
1050+
1051+
- `'a'` and `'r+'` create the array and fail if a node exists at `path`,
1052+
unless `overwrite` is `True`.
1053+
- `'w'` replaces anything stored under `path` (the whole store if `path` is
1054+
not set), even if `overwrite` is `False`. On a store that cannot delete
1055+
keys, `'w'` raises an error instead of replacing an existing node.
1056+
- `'w-'` fails if anything is stored under `path`, even if `overwrite` is
1057+
`True`.
1058+
- `'r'` always fails.
1059+
1060+
`mode` has no effect, and is not validated, if `store` is a `StorePath`.
1061+
data : array-like, optional
1062+
Values written into the new array after it is created. Unlike the `data`
1063+
parameter of `create_array`, it does not set the shape or data type of the
1064+
array. To create an array from existing data, use `create_array(data=...)`.
1065+
A Zarr array is not supported as `data`.
10451066
10461067
Returns
10471068
-------
@@ -1066,7 +1087,6 @@ async def create(
10661087
if write_empty_chunks is not None:
10671088
_warn_write_empty_chunks_kwarg()
10681089

1069-
mode = kwargs.pop("mode", None)
10701090
if mode is None:
10711091
mode = "a"
10721092
store_path = await make_store_path(store, path=path, mode=mode, storage_options=storage_options)
@@ -1102,7 +1122,7 @@ async def create(
11021122
dimension_names=dimension_names,
11031123
attributes=attributes,
11041124
config=config_parsed,
1105-
**kwargs,
1125+
data=data,
11061126
)
11071127

11081128

‎src/zarr/api/synchronous.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,6 @@ def create_group(
630630
)
631631

632632

633-
# TODO: add type annotations for kwargs
634633
def create(
635634
shape: tuple[int, ...] | int,
636635
*, # Note: this is a change from v2
@@ -666,7 +665,8 @@ def create(
666665
dimension_names: DimensionNamesLike = None,
667666
storage_options: dict[str, Any] | None = None,
668667
config: ArrayConfigLike | None = None,
669-
**kwargs: Any,
668+
mode: AccessModeLiteral | None = None,
669+
data: npt.ArrayLike | None = None,
670670
) -> AnyArray:
671671
"""Create an array.
672672
@@ -786,6 +786,26 @@ def create(
786786
config : ArrayConfigLike, optional
787787
Runtime configuration of the array. If provided, will override the
788788
default values from `zarr.config.array`.
789+
mode : {'r', 'r+', 'a', 'w', 'w-'}, optional
790+
Legacy way to control overwriting, kept for compatibility with Zarr-Python 2.
791+
Prefer `overwrite`. The access mode used to open `store`; the default, `None`,
792+
is `'a'`.
793+
794+
- `'a'` and `'r+'` create the array and fail if a node exists at `path`,
795+
unless `overwrite` is `True`.
796+
- `'w'` replaces anything stored under `path` (the whole store if `path` is
797+
not set), even if `overwrite` is `False`. On a store that cannot delete
798+
keys, `'w'` raises an error instead of replacing an existing node.
799+
- `'w-'` fails if anything is stored under `path`, even if `overwrite` is
800+
`True`.
801+
- `'r'` always fails.
802+
803+
`mode` has no effect, and is not validated, if `store` is a `StorePath`.
804+
data : array-like, optional
805+
Values written into the new array after it is created. Unlike the `data`
806+
parameter of `create_array`, it does not set the shape or data type of the
807+
array. To create an array from existing data, use `create_array(data=...)`.
808+
A Zarr array is not supported as `data`.
789809
790810
Returns
791811
-------
@@ -822,7 +842,8 @@ def create(
822842
dimension_names=dimension_names,
823843
storage_options=storage_options,
824844
config=config,
825-
**kwargs,
845+
mode=mode,
846+
data=data,
826847
)
827848
)
828849
)

‎tests/test_api.py‎

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
from collections.abc import Callable
1515
from pathlib import Path
1616

17+
import numpy.typing as npt
18+
1719
from zarr.abc.store import Store
18-
from zarr.core.common import JSON, MemoryOrder, ZarrFormat
20+
from zarr.core.common import JSON, AccessModeLiteral, MemoryOrder, ZarrFormat
1921
from zarr.types import AnyArray
2022

2123
import contextlib
@@ -43,9 +45,11 @@
4345
save_array,
4446
save_group,
4547
)
46-
from zarr.core.buffer import NDArrayLike
48+
from zarr.core.buffer import NDArrayLike, default_buffer_prototype
49+
from zarr.core.sync import sync
4750
from zarr.errors import (
4851
ArrayNotFoundError,
52+
ContainsArrayError,
4953
MetadataValidationError,
5054
ZarrDeprecationWarning,
5155
ZarrUserWarning,
@@ -84,6 +88,117 @@ def test_create(memory_store: Store) -> None:
8488
z = create(shape=(400, 100), chunks=(16, 16.5), store=store, overwrite=True) # type: ignore[arg-type]
8589

8690

91+
def _create_async(**kwargs: Any) -> AnyArray:
92+
return Array(sync(zarr.api.asynchronous.create(**kwargs)))
93+
94+
95+
CREATE_FUNCS = pytest.mark.parametrize(
96+
"create_func", [create, _create_async], ids=["sync", "async"]
97+
)
98+
99+
100+
def _write_existing_array(store: MemoryStore) -> None:
101+
"""
102+
Store an array with written chunks, plus a key that belongs to no Zarr node.
103+
"""
104+
old = create_array(store, shape=(4,), chunks=(2,), dtype="int8")
105+
old[:] = [1, 2, 3, 4]
106+
sync(store.set("junk", default_buffer_prototype().buffer.from_bytes(b"junk")))
107+
108+
109+
@CREATE_FUNCS
110+
@pytest.mark.parametrize(
111+
("mode", "overwrite", "existing"),
112+
[
113+
(None, False, False),
114+
("a", False, False),
115+
("r+", False, False),
116+
("w", False, False),
117+
("w-", False, False),
118+
("a", True, True),
119+
("r+", True, True),
120+
("w", False, True),
121+
("w", True, True),
122+
],
123+
ids=lambda v: repr(v),
124+
)
125+
@pytest.mark.parametrize("data", [None, np.arange(3, dtype="int8")], ids=["no-data", "int8-data"])
126+
def test_create_mode_data(
127+
create_func: Callable[..., AnyArray],
128+
mode: AccessModeLiteral | None,
129+
overwrite: bool,
130+
existing: bool,
131+
data: npt.NDArray[Any] | None,
132+
) -> None:
133+
"""
134+
`create` opens the store with `mode`, replaces what is stored under the path when
135+
`mode` or `overwrite` says so, and writes `data` without taking its data type.
136+
"""
137+
store = MemoryStore()
138+
if existing:
139+
_write_existing_array(store)
140+
reference = MemoryStore()
141+
create(shape=(3,), chunks=(2,), store=reference, data=data)
142+
143+
z = create_func(shape=(3,), chunks=(2,), store=store, mode=mode, overwrite=overwrite, data=data)
144+
145+
assert sorted(store._store_dict) == sorted(reference._store_dict)
146+
assert z.dtype == np.dtype("float64")
147+
assert_array_equal(z[:], np.zeros(3) if data is None else data)
148+
149+
150+
@pytest.mark.parametrize("mode", [None, "a", "r+"])
151+
def test_create_existing_array_without_overwrite(mode: AccessModeLiteral | None) -> None:
152+
"""
153+
`create` refuses to replace an existing array unless `mode` or `overwrite` allows it.
154+
"""
155+
store = MemoryStore()
156+
_write_existing_array(store)
157+
with pytest.raises(ContainsArrayError):
158+
create(shape=(3,), store=store, mode=mode)
159+
160+
161+
@pytest.mark.parametrize("overwrite", [False, True])
162+
def test_create_mode_w_minus_existing_data(overwrite: bool) -> None:
163+
"""
164+
`create` with mode `'w-'` refuses to write where data is stored, whatever `overwrite` says.
165+
"""
166+
store = MemoryStore()
167+
_write_existing_array(store)
168+
with pytest.raises(FileExistsError, match="mode 'w-'"):
169+
create(shape=(3,), store=store, mode="w-", overwrite=overwrite)
170+
171+
172+
def test_create_mode_r() -> None:
173+
"""
174+
`create` with mode `'r'` opens the store read-only, so creation fails.
175+
"""
176+
with pytest.raises(ValueError, match="read-only"):
177+
create(shape=(3,), store=MemoryStore(), mode="r")
178+
179+
180+
def test_create_invalid_mode() -> None:
181+
"""
182+
`create` rejects a mode that is not an access mode.
183+
"""
184+
with pytest.raises(ValueError, match="Invalid mode: x"):
185+
create(shape=(3,), store=MemoryStore(), mode="x") # type: ignore[arg-type]
186+
187+
188+
@CREATE_FUNCS
189+
def test_create_unknown_keyword(create_func: Callable[..., AnyArray]) -> None:
190+
"""
191+
`create` rejects keyword arguments it does not declare, naming itself in the error,
192+
before `mode="w"` deletes anything.
193+
"""
194+
store = MemoryStore()
195+
_write_existing_array(store)
196+
before = dict(store._store_dict)
197+
with pytest.raises(TypeError, match=r"^create\(\) got an unexpected keyword argument 'typo'"):
198+
create_func(shape=(3,), store=store, mode="w", typo=1)
199+
assert store._store_dict == before
200+
201+
87202
@pytest.mark.parametrize(
88203
"func",
89204
[

0 commit comments

Comments
 (0)