Skip to content

Commit ad3df88

Browse files
committed
Merge branch 'v3' into generalize-stateful-store
* v3: chore: update pre-commit hooks (zarr-developers#2222) fix: validate v3 dtypes when loading/creating v3 metadata (zarr-developers#2209) fix typo in store integration test (zarr-developers#2223) Basic Zarr-python 2.x compatibility changes (zarr-developers#2098) Make Group.arrays, groups compatible with v2 (zarr-developers#2213) Typing fixes to test_indexing (zarr-developers#2193) Default to RemoteStore for fsspec URIs (zarr-developers#2198) Make MemoryStore serialiazable (zarr-developers#2204) [v3] Implement Group methods for empty, full, ones, and zeros (zarr-developers#2210) implement `store.list_prefix` and `store._set_many` (zarr-developers#2064) Fixed codec for v2 data with no fill value (zarr-developers#2207)
2 parents 7e8c1c2 + cd7321b commit ad3df88

34 files changed

+1321
-520
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ default_language_version:
77
python: python3
88
repos:
99
- repo: https://github.com/astral-sh/ruff-pre-commit
10-
rev: v0.6.5
10+
rev: v0.6.7
1111
hooks:
1212
- id: ruff
1313
args: ["--fix", "--show-fixes"]

pyproject.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ test = [
6666
"flask",
6767
"requests",
6868
"mypy",
69-
"hypothesis"
69+
"hypothesis",
70+
"universal-pathlib",
7071
]
7172

7273
jupyter = [
@@ -273,6 +274,7 @@ filterwarnings = [
273274
"ignore:PY_SSIZE_T_CLEAN will be required.*:DeprecationWarning",
274275
"ignore:The loop argument is deprecated since Python 3.8.*:DeprecationWarning",
275276
"ignore:Creating a zarr.buffer.gpu.*:UserWarning",
277+
"ignore:Duplicate name:UserWarning", # from ZipFile
276278
]
277279
markers = [
278280
"gpu: mark a test as requiring CuPy and GPU"

src/zarr/_compat.py

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import warnings
2+
from collections.abc import Callable
3+
from functools import wraps
4+
from inspect import Parameter, signature
5+
from typing import Any, TypeVar
6+
7+
T = TypeVar("T")
8+
9+
# Based off https://github.com/scikit-learn/scikit-learn/blob/e87b32a81c70abed8f2e97483758eb64df8255e9/sklearn/utils/validation.py#L63
10+
11+
12+
def _deprecate_positional_args(
13+
func: Callable[..., T] | None = None, *, version: str = "3.1.0"
14+
) -> Callable[..., T]:
15+
"""Decorator for methods that issues warnings for positional arguments.
16+
17+
Using the keyword-only argument syntax in pep 3102, arguments after the
18+
* will issue a warning when passed as a positional argument.
19+
20+
Parameters
21+
----------
22+
func : callable, default=None
23+
Function to check arguments on.
24+
version : callable, default="3.1.0"
25+
The version when positional arguments will result in error.
26+
"""
27+
28+
def _inner_deprecate_positional_args(f: Callable[..., T]) -> Callable[..., T]:
29+
sig = signature(f)
30+
kwonly_args = []
31+
all_args = []
32+
33+
for name, param in sig.parameters.items():
34+
if param.kind == Parameter.POSITIONAL_OR_KEYWORD:
35+
all_args.append(name)
36+
elif param.kind == Parameter.KEYWORD_ONLY:
37+
kwonly_args.append(name)
38+
39+
@wraps(f)
40+
def inner_f(*args: Any, **kwargs: Any) -> T:
41+
extra_args = len(args) - len(all_args)
42+
if extra_args <= 0:
43+
return f(*args, **kwargs)
44+
45+
# extra_args > 0
46+
args_msg = [
47+
f"{name}={arg}"
48+
for name, arg in zip(kwonly_args[:extra_args], args[-extra_args:], strict=False)
49+
]
50+
formatted_args_msg = ", ".join(args_msg)
51+
warnings.warn(
52+
(
53+
f"Pass {formatted_args_msg} as keyword args. From version "
54+
f"{version} passing these as positional arguments "
55+
"will result in an error"
56+
),
57+
FutureWarning,
58+
stacklevel=2,
59+
)
60+
kwargs.update(zip(sig.parameters, args, strict=False))
61+
return f(**kwargs)
62+
63+
return inner_f
64+
65+
if func is not None:
66+
return _inner_deprecate_positional_args(func)
67+
68+
return _inner_deprecate_positional_args # type: ignore[return-value]

src/zarr/abc/store.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC, abstractmethod
2-
from collections.abc import AsyncGenerator
2+
from asyncio import gather
3+
from collections.abc import AsyncGenerator, Iterable
34
from typing import Any, NamedTuple, Protocol, runtime_checkable
45

56
from typing_extensions import Self
@@ -158,6 +159,13 @@ async def set(self, key: str, value: Buffer) -> None:
158159
"""
159160
...
160161

162+
async def _set_many(self, values: Iterable[tuple[str, Buffer]]) -> None:
163+
"""
164+
Insert multiple (key, value) pairs into storage.
165+
"""
166+
await gather(*(self.set(key, value) for key, value in values))
167+
return None
168+
161169
@property
162170
@abstractmethod
163171
def supports_deletes(self) -> bool:
@@ -211,7 +219,9 @@ def list(self) -> AsyncGenerator[str, None]:
211219

212220
@abstractmethod
213221
def list_prefix(self, prefix: str) -> AsyncGenerator[str, None]:
214-
"""Retrieve all keys in the store with a given prefix.
222+
"""
223+
Retrieve all keys in the store that begin with a given prefix. Keys are returned with the
224+
common leading prefix removed.
215225
216226
Parameters
217227
----------

src/zarr/api/asynchronous.py

+54-17
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ async def open(
194194
zarr_version: ZarrFormat | None = None, # deprecated
195195
zarr_format: ZarrFormat | None = None,
196196
path: str | None = None,
197+
storage_options: dict[str, Any] | None = None,
197198
**kwargs: Any, # TODO: type kwargs as valid args to open_array
198199
) -> AsyncArray | AsyncGroup:
199200
"""Convenience function to open a group or array using file-mode-like semantics.
@@ -211,6 +212,9 @@ async def open(
211212
The zarr format to use when saving.
212213
path : str or None, optional
213214
The path within the store to open.
215+
storage_options : dict
216+
If using an fsspec URL to create the store, these will be passed to
217+
the backend implementation. Ignored otherwise.
214218
**kwargs
215219
Additional parameters are passed through to :func:`zarr.creation.open_array` or
216220
:func:`zarr.hierarchy.open_group`.
@@ -221,7 +225,7 @@ async def open(
221225
Return type depends on what exists in the given store.
222226
"""
223227
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
224-
store_path = await make_store_path(store, mode=mode)
228+
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
225229

226230
if path is not None:
227231
store_path = store_path / path
@@ -276,6 +280,7 @@ async def save_array(
276280
zarr_version: ZarrFormat | None = None, # deprecated
277281
zarr_format: ZarrFormat | None = None,
278282
path: str | None = None,
283+
storage_options: dict[str, Any] | None = None,
279284
**kwargs: Any, # TODO: type kwargs as valid args to create
280285
) -> None:
281286
"""Convenience function to save a NumPy array to the local file system, following a
@@ -291,6 +296,9 @@ async def save_array(
291296
The zarr format to use when saving.
292297
path : str or None, optional
293298
The path within the store where the array will be saved.
299+
storage_options : dict
300+
If using an fsspec URL to create the store, these will be passed to
301+
the backend implementation. Ignored otherwise.
294302
kwargs
295303
Passed through to :func:`create`, e.g., compressor.
296304
"""
@@ -299,7 +307,7 @@ async def save_array(
299307
or _default_zarr_version()
300308
)
301309

302-
store_path = await make_store_path(store, mode="w")
310+
store_path = await make_store_path(store, mode="w", storage_options=storage_options)
303311
if path is not None:
304312
store_path = store_path / path
305313
new = await AsyncArray.create(
@@ -319,6 +327,7 @@ async def save_group(
319327
zarr_version: ZarrFormat | None = None, # deprecated
320328
zarr_format: ZarrFormat | None = None,
321329
path: str | None = None,
330+
storage_options: dict[str, Any] | None = None,
322331
**kwargs: NDArrayLike,
323332
) -> None:
324333
"""Convenience function to save several NumPy arrays to the local file system, following a
@@ -334,22 +343,40 @@ async def save_group(
334343
The zarr format to use when saving.
335344
path : str or None, optional
336345
Path within the store where the group will be saved.
346+
storage_options : dict
347+
If using an fsspec URL to create the store, these will be passed to
348+
the backend implementation. Ignored otherwise.
337349
kwargs
338350
NumPy arrays with data to save.
339351
"""
340352
zarr_format = (
341-
_handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
353+
_handle_zarr_version_or_format(
354+
zarr_version=zarr_version,
355+
zarr_format=zarr_format,
356+
)
342357
or _default_zarr_version()
343358
)
344359

345360
if len(args) == 0 and len(kwargs) == 0:
346361
raise ValueError("at least one array must be provided")
347362
aws = []
348363
for i, arr in enumerate(args):
349-
aws.append(save_array(store, arr, zarr_format=zarr_format, path=f"{path}/arr_{i}"))
364+
aws.append(
365+
save_array(
366+
store,
367+
arr,
368+
zarr_format=zarr_format,
369+
path=f"{path}/arr_{i}",
370+
storage_options=storage_options,
371+
)
372+
)
350373
for k, arr in kwargs.items():
351374
_path = f"{path}/{k}" if path is not None else k
352-
aws.append(save_array(store, arr, zarr_format=zarr_format, path=_path))
375+
aws.append(
376+
save_array(
377+
store, arr, zarr_format=zarr_format, path=_path, storage_options=storage_options
378+
)
379+
)
353380
await asyncio.gather(*aws)
354381

355382

@@ -418,6 +445,7 @@ async def group(
418445
zarr_format: ZarrFormat | None = None,
419446
meta_array: Any | None = None, # not used
420447
attributes: dict[str, JSON] | None = None,
448+
storage_options: dict[str, Any] | None = None,
421449
) -> AsyncGroup:
422450
"""Create a group.
423451
@@ -444,6 +472,9 @@ async def group(
444472
to users. Use `numpy.empty(())` by default.
445473
zarr_format : {2, 3, None}, optional
446474
The zarr format to use when saving.
475+
storage_options : dict
476+
If using an fsspec URL to create the store, these will be passed to
477+
the backend implementation. Ignored otherwise.
447478
448479
Returns
449480
-------
@@ -453,7 +484,7 @@ async def group(
453484

454485
zarr_format = _handle_zarr_version_or_format(zarr_version=zarr_version, zarr_format=zarr_format)
455486

456-
store_path = await make_store_path(store)
487+
store_path = await make_store_path(store, storage_options=storage_options)
457488
if path is not None:
458489
store_path = store_path / path
459490

@@ -472,7 +503,7 @@ async def group(
472503
try:
473504
return await AsyncGroup.open(store=store_path, zarr_format=zarr_format)
474505
except (KeyError, FileNotFoundError):
475-
return await AsyncGroup.create(
506+
return await AsyncGroup.from_store(
476507
store=store_path,
477508
zarr_format=zarr_format or _default_zarr_version(),
478509
exists_ok=overwrite,
@@ -481,14 +512,14 @@ async def group(
481512

482513

483514
async def open_group(
484-
*, # Note: this is a change from v2
485515
store: StoreLike | None = None,
516+
*, # Note: this is a change from v2
486517
mode: AccessModeLiteral | None = None,
487518
cache_attrs: bool | None = None, # not used, default changed
488519
synchronizer: Any = None, # not used
489520
path: str | None = None,
490521
chunk_store: StoreLike | None = None, # not used
491-
storage_options: dict[str, Any] | None = None, # not used
522+
storage_options: dict[str, Any] | None = None,
492523
zarr_version: ZarrFormat | None = None, # deprecated
493524
zarr_format: ZarrFormat | None = None,
494525
meta_array: Any | None = None, # not used
@@ -548,10 +579,8 @@ async def open_group(
548579
warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2)
549580
if chunk_store is not None:
550581
warnings.warn("chunk_store is not yet implemented", RuntimeWarning, stacklevel=2)
551-
if storage_options is not None:
552-
warnings.warn("storage_options is not yet implemented", RuntimeWarning, stacklevel=2)
553582

554-
store_path = await make_store_path(store, mode=mode)
583+
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
555584
if path is not None:
556585
store_path = store_path / path
557586

@@ -561,7 +590,7 @@ async def open_group(
561590
try:
562591
return await AsyncGroup.open(store_path, zarr_format=zarr_format)
563592
except (KeyError, FileNotFoundError):
564-
return await AsyncGroup.create(
593+
return await AsyncGroup.from_store(
565594
store_path,
566595
zarr_format=zarr_format or _default_zarr_version(),
567596
exists_ok=True,
@@ -575,7 +604,7 @@ async def create(
575604
chunks: ChunkCoords | None = None, # TODO: v2 allowed chunks=True
576605
dtype: npt.DTypeLike | None = None,
577606
compressor: dict[str, JSON] | None = None, # TODO: default and type change
578-
fill_value: Any = 0, # TODO: need type
607+
fill_value: Any | None = 0, # TODO: need type
579608
order: MemoryOrder | None = None, # TODO: default change
580609
store: str | StoreLike | None = None,
581610
synchronizer: Any | None = None,
@@ -603,6 +632,7 @@ async def create(
603632
) = None,
604633
codecs: Iterable[Codec | dict[str, JSON]] | None = None,
605634
dimension_names: Iterable[str] | None = None,
635+
storage_options: dict[str, Any] | None = None,
606636
**kwargs: Any,
607637
) -> AsyncArray:
608638
"""Create an array.
@@ -674,6 +704,9 @@ async def create(
674704
to users. Use `numpy.empty(())` by default.
675705
676706
.. versionadded:: 2.13
707+
storage_options : dict
708+
If using an fsspec URL to create the store, these will be passed to
709+
the backend implementation. Ignored otherwise.
677710
678711
Returns
679712
-------
@@ -725,7 +758,7 @@ async def create(
725758
warnings.warn("meta_array is not yet implemented", RuntimeWarning, stacklevel=2)
726759

727760
mode = kwargs.pop("mode", cast(AccessModeLiteral, "r" if read_only else "w"))
728-
store_path = await make_store_path(store, mode=mode)
761+
store_path = await make_store_path(store, mode=mode, storage_options=storage_options)
729762
if path is not None:
730763
store_path = store_path / path
731764

@@ -827,7 +860,7 @@ async def full_like(a: ArrayLike, **kwargs: Any) -> AsyncArray:
827860
"""
828861
like_kwargs = _like_args(a, kwargs)
829862
if isinstance(a, AsyncArray):
830-
kwargs.setdefault("fill_value", a.metadata.fill_value)
863+
like_kwargs.setdefault("fill_value", a.metadata.fill_value)
831864
return await full(**like_kwargs)
832865

833866

@@ -875,6 +908,7 @@ async def open_array(
875908
zarr_version: ZarrFormat | None = None, # deprecated
876909
zarr_format: ZarrFormat | None = None,
877910
path: PathLike | None = None,
911+
storage_options: dict[str, Any] | None = None,
878912
**kwargs: Any, # TODO: type kwargs as valid args to save
879913
) -> AsyncArray:
880914
"""Open an array using file-mode-like semantics.
@@ -887,6 +921,9 @@ async def open_array(
887921
The zarr format to use when saving.
888922
path : string, optional
889923
Path in store to array.
924+
storage_options : dict
925+
If using an fsspec URL to create the store, these will be passed to
926+
the backend implementation. Ignored otherwise.
890927
**kwargs
891928
Any keyword arguments to pass to the array constructor.
892929
@@ -896,7 +933,7 @@ async def open_array(
896933
The opened array.
897934
"""
898935

899-
store_path = await make_store_path(store)
936+
store_path = await make_store_path(store, storage_options=storage_options)
900937
if path is not None:
901938
store_path = store_path / path
902939

0 commit comments

Comments
 (0)