From 88b9583c55a8fe56880cd0f151e1d4d6888583c2 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 8 Oct 2024 09:37:25 -0500 Subject: [PATCH 1/6] zarr.open should fall back to opening a group Closes https://github.com/zarr-developers/zarr-python/issues/2309 --- src/zarr/api/asynchronous.py | 7 ++++++- src/zarr/api/synchronous.py | 2 ++ tests/v3/test_api.py | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index 62a973257c..acc18f3678 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -247,7 +247,10 @@ async def open( try: return await open_array(store=store_path, zarr_format=zarr_format, **kwargs) - except KeyError: + except (KeyError, ValueError): + # KeyError for a missing key + # ValueError 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) @@ -580,6 +583,8 @@ async def open_group( meta_array : array-like, optional An array instance to use for determining arrays to create and return to users. Use `numpy.empty(())` by default. + attributes : dict + A dictionary of JSON-serializable values with user-defined attributes. Returns ------- diff --git a/src/zarr/api/synchronous.py b/src/zarr/api/synchronous.py index f5d614058a..d76216b781 100644 --- a/src/zarr/api/synchronous.py +++ b/src/zarr/api/synchronous.py @@ -207,6 +207,7 @@ def open_group( zarr_version: ZarrFormat | None = None, # deprecated zarr_format: ZarrFormat | None = None, meta_array: Any | None = None, # not used in async api + attributes: dict[str, JSON] | None = None, ) -> Group: return Group( sync( @@ -221,6 +222,7 @@ def open_group( zarr_version=zarr_version, zarr_format=zarr_format, meta_array=meta_array, + attributes=attributes, ) ) ) diff --git a/tests/v3/test_api.py b/tests/v3/test_api.py index 218aec5c97..22e474e98f 100644 --- a/tests/v3/test_api.py +++ b/tests/v3/test_api.py @@ -6,6 +6,7 @@ from numpy.testing import assert_array_equal import zarr +import zarr.api.asynchronous from zarr import Array, Group from zarr.abc.store import Store from zarr.api.synchronous import create, group, load, open, open_group, save, save_array, save_group @@ -921,3 +922,23 @@ def test_open_group_positional_args_deprecated() -> None: store = MemoryStore({}, mode="w") 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") + zarr.open_group(store, attributes={"key": "value"}) + + group = zarr.open(store) + assert isinstance(group, Group) + assert group.attrs == {"key": "value"} + + +async def test_open_falls_back_to_open_group_async() -> None: + # https://github.com/zarr-developers/zarr-python/issues/2309 + store = MemoryStore(mode="w") + await zarr.api.asynchronous.open_group(store, attributes={"key": "value"}) + + group = await zarr.api.asynchronous.open(store=store) + assert isinstance(group, zarr.api.asynchronous.AsyncGroup) + assert group.attrs == {"key": "value"} From 9be5ef1ff58adc8888f04eadaf4b1666f0c38ad2 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 8 Oct 2024 10:25:05 -0500 Subject: [PATCH 2/6] fixup --- tests/v3/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/v3/test_api.py b/tests/v3/test_api.py index 22e474e98f..eb5533e0e9 100644 --- a/tests/v3/test_api.py +++ b/tests/v3/test_api.py @@ -7,6 +7,7 @@ import zarr import zarr.api.asynchronous +import zarr.core.group from zarr import Array, Group from zarr.abc.store import Store from zarr.api.synchronous import create, group, load, open, open_group, save, save_array, save_group @@ -940,5 +941,5 @@ async def test_open_falls_back_to_open_group_async() -> None: await zarr.api.asynchronous.open_group(store, attributes={"key": "value"}) group = await zarr.api.asynchronous.open(store=store) - assert isinstance(group, zarr.api.asynchronous.AsyncGroup) + assert isinstance(group, zarr.core.group.AsyncGroup) assert group.attrs == {"key": "value"} From 3878f2401d3deb73727eb7b767ad11011b3ce6e1 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 8 Oct 2024 10:33:51 -0500 Subject: [PATCH 3/6] robuster --- src/zarr/api/asynchronous.py | 5 +++-- src/zarr/core/metadata/v3.py | 5 +++-- src/zarr/errors.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/zarr/api/asynchronous.py b/src/zarr/api/asynchronous.py index acc18f3678..8cbadd9f81 100644 --- a/src/zarr/api/asynchronous.py +++ b/src/zarr/api/asynchronous.py @@ -14,6 +14,7 @@ from zarr.core.group import AsyncGroup from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata +from zarr.errors import NodeTypeValidationError from zarr.storage import ( StoreLike, StorePath, @@ -247,9 +248,9 @@ async def open( try: return await open_array(store=store_path, zarr_format=zarr_format, **kwargs) - except (KeyError, ValueError): + except (KeyError, NodeTypeValidationError): # KeyError for a missing key - # ValueError for failing to parse node metadata as an array when it's + # 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) diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py index 692f778566..8f80781ffa 100644 --- a/src/zarr/core/metadata/v3.py +++ b/src/zarr/core/metadata/v3.py @@ -28,6 +28,7 @@ from zarr.core.common import ZARR_JSON, parse_named_configuration, parse_shapelike from zarr.core.config import config from zarr.core.metadata.common import ArrayMetadata, parse_attributes +from zarr.errors import MetadataValidationError, NodeTypeValidationError from zarr.registry import get_codec_class DEFAULT_DTYPE = "float64" @@ -36,13 +37,13 @@ def parse_zarr_format(data: object) -> Literal[3]: if data == 3: return 3 - raise ValueError(f"Invalid value. Expected 3. Got {data}.") + raise MetadataValidationError(f"Invalid value. Expected 3. Got {data}.") def parse_node_type_array(data: object) -> Literal["array"]: if data == "array": return "array" - raise ValueError(f"Invalid value. Expected 'array'. Got {data}.") + raise NodeTypeValidationError(f"Invalid value. Expected 'array'. Got {data}.") def parse_codecs(data: object) -> tuple[Codec, ...]: diff --git a/src/zarr/errors.py b/src/zarr/errors.py index 72efcedf2e..6bb07a68e1 100644 --- a/src/zarr/errors.py +++ b/src/zarr/errors.py @@ -25,6 +25,19 @@ class ContainsArrayAndGroupError(_BaseZarrError): ) +class MetadataValidationError(_BaseZarrError): + """An exception raised when the Zarr metadata is invalid in some way""" + + +class NodeTypeValidationError(MetadataValidationError): + """ + Specialized exception when the node_type of the metadata document is incorrect.. + + This can be raised when the value is invalid or unexpected given the context, + for example an 'array' node when we expected a 'group'. + """ + + __all__ = [ "ContainsArrayAndGroupError", "ContainsArrayError", From 98ba6ca6cd52717457bde8d87b87541f4b0c820f Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Tue, 8 Oct 2024 20:41:37 -0500 Subject: [PATCH 4/6] fixup test --- tests/v3/test_metadata/test_v3.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/v3/test_metadata/test_v3.py b/tests/v3/test_metadata/test_v3.py index 3eb6682e63..6d433b5f0e 100644 --- a/tests/v3/test_metadata/test_v3.py +++ b/tests/v3/test_metadata/test_v3.py @@ -24,6 +24,7 @@ default_fill_value, parse_dimension_names, parse_fill_value, + parse_node_type_array, parse_zarr_format, ) @@ -62,6 +63,16 @@ def test_parse_zarr_format_valid() -> None: assert parse_zarr_format(3) == 3 +@pytest.mark.parametrize("data", [None, "group"]) +def test_parse_node_type_arrayinvalid(data: Any) -> None: + with pytest.raises(ValueError, match=f"Invalid value. Expected 'array'. Got '{data}'."): + parse_node_type_array(data) + + +def test_parse_node_typevalid() -> None: + assert parse_node_type_array("array") == "array" + + @pytest.mark.parametrize("data", [(), [1, 2, "a"], {"foo": 10}]) def parse_dimension_names_invalid(data: Any) -> None: with pytest.raises(TypeError, match="Expected either None or iterable of str,"): From d581b4e631da41de782e7daf000c97e822534e89 Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 9 Oct 2024 20:34:13 -0500 Subject: [PATCH 5/6] Consistent version error --- src/zarr/core/array.py | 3 +- src/zarr/core/group.py | 3 +- src/zarr/core/metadata/v3.py | 2 +- src/zarr/errors.py | 2 +- tests/v3/test_api.py | 945 ----------------------------------- 5 files changed, 6 insertions(+), 949 deletions(-) diff --git a/src/zarr/core/array.py b/src/zarr/core/array.py index 8d63d9c321..fdf9aa623e 100644 --- a/src/zarr/core/array.py +++ b/src/zarr/core/array.py @@ -67,6 +67,7 @@ from zarr.core.metadata.v2 import ArrayV2Metadata from zarr.core.metadata.v3 import ArrayV3Metadata from zarr.core.sync import collect_aiterator, sync +from zarr.errors import MetadataValidationError from zarr.registry import get_pipeline_class from zarr.storage import StoreLike, make_store_path from zarr.storage.common import StorePath, ensure_no_existing_node @@ -144,7 +145,7 @@ async def get_array_metadata( else: zarr_format = 2 else: - raise ValueError(f"unexpected zarr_format: {zarr_format}") + raise MetadataValidationError("zarr_format", "2, 3, or None", zarr_format) metadata_dict: dict[str, Any] if zarr_format == 2: diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index bf0e385d06..4d43570325 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -29,6 +29,7 @@ ) from zarr.core.config import config from zarr.core.sync import SyncMixin, sync +from zarr.errors import MetadataValidationError from zarr.storage import StoreLike, make_store_path from zarr.storage.common import StorePath, ensure_no_existing_node @@ -196,7 +197,7 @@ async def open( else: zarr_format = 2 else: - raise ValueError(f"unexpected zarr_format: {zarr_format}") + raise MetadataValidationError("zarr_format", "2, 3, or None", zarr_format) if zarr_format == 2: # V2 groups are comprised of a .zgroup and .zattrs objects diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py index 78a71b1484..215b983b6e 100644 --- a/src/zarr/core/metadata/v3.py +++ b/src/zarr/core/metadata/v3.py @@ -38,7 +38,7 @@ def parse_zarr_format(data: object) -> Literal[3]: if data == 3: return 3 - raise MetadataValidationError(3, data) + raise MetadataValidationError("zarr_format", 3, data) def parse_node_type_array(data: object) -> Literal["array"]: diff --git a/src/zarr/errors.py b/src/zarr/errors.py index a455651456..57045fccd3 100644 --- a/src/zarr/errors.py +++ b/src/zarr/errors.py @@ -28,7 +28,7 @@ class ContainsArrayAndGroupError(_BaseZarrError): class MetadataValidationError(_BaseZarrError): """An exception raised when the Zarr metadata is invalid in some way""" - _msg = "Invalid value. Expected '{}'. Got '{}'." + _msg = "Invalid value for '{}'. Expected '{}'. Got '{}'." class NodeTypeValidationError(MetadataValidationError): diff --git a/tests/v3/test_api.py b/tests/v3/test_api.py index eb5533e0e9..e69de29bb2 100644 --- a/tests/v3/test_api.py +++ b/tests/v3/test_api.py @@ -1,945 +0,0 @@ -import pathlib -import warnings - -import numpy as np -import pytest -from numpy.testing import assert_array_equal - -import zarr -import zarr.api.asynchronous -import zarr.core.group -from zarr import Array, Group -from zarr.abc.store import Store -from zarr.api.synchronous import create, group, load, open, open_group, save, save_array, save_group -from zarr.core.common import ZarrFormat -from zarr.storage.memory import MemoryStore - - -def test_create_array(memory_store: Store) -> None: - store = memory_store - - # create array - z = create(shape=100, store=store) - assert isinstance(z, Array) - assert z.shape == (100,) - - # create array, overwrite, specify chunk shape - z = create(shape=200, chunk_shape=20, store=store, overwrite=True) - assert isinstance(z, Array) - assert z.shape == (200,) - assert z.chunks == (20,) - - # create array, overwrite, specify chunk shape via chunks param - z = create(shape=400, chunks=40, store=store, overwrite=True) - assert isinstance(z, Array) - assert z.shape == (400,) - assert z.chunks == (40,) - - -async def test_open_array(memory_store: MemoryStore) -> None: - store = memory_store - - # open array, create if doesn't exist - z = open(store=store, shape=100) - assert isinstance(z, Array) - assert z.shape == (100,) - - # open array, overwrite - # store._store_dict = {} - store = MemoryStore(mode="w") - 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) - assert isinstance(z, Array) - assert z.shape == (200,) - assert z.read_only - - # path not found - with pytest.raises(FileNotFoundError): - open(store="doesnotexist", mode="r") - - -async def test_open_group(memory_store: MemoryStore) -> None: - store = memory_store - - # open group, create if doesn't exist - g = open_group(store=store) - g.create_group("foo") - assert isinstance(g, Group) - assert "foo" in g - - # open group, overwrite - # g = open_group(store=store) - # assert isinstance(g, Group) - # assert "foo" not in g - - # 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) - assert isinstance(g, Group) - # assert g.read_only - - -@pytest.mark.parametrize("zarr_format", [None, 2, 3]) -async def test_open_group_unspecified_version( - tmpdir: pathlib.Path, zarr_format: ZarrFormat -) -> None: - """regression test for https://github.com/zarr-developers/zarr-python/issues/2175""" - - # create a group with specified zarr format (could be 2, 3, or None) - _ = await zarr.api.asynchronous.open_group( - store=str(tmpdir), mode="w", zarr_format=zarr_format, attributes={"foo": "bar"} - ) - - # now open that group without specifying the format - g2 = await zarr.api.asynchronous.open_group(store=str(tmpdir), mode="r") - - assert g2.attrs == {"foo": "bar"} - - if zarr_format is not None: - assert g2.metadata.zarr_format == zarr_format - - -def test_save_errors() -> None: - with pytest.raises(ValueError): - # no arrays provided - save_group("data/group.zarr") - with pytest.raises(TypeError): - # no array provided - save_array("data/group.zarr") - with pytest.raises(ValueError): - # no arrays provided - save("data/group.zarr") - - -def test_open_with_mode_r(tmp_path: pathlib.Path) -> None: - # 'r' means read only (must exist) - with pytest.raises(FileNotFoundError): - zarr.open(store=tmp_path, mode="r") - z1 = zarr.ones(store=tmp_path, shape=(3, 3)) - assert z1.fill_value == 1 - z2 = zarr.open(store=tmp_path, mode="r") - assert isinstance(z2, Array) - assert z2.fill_value == 1 - assert (z2[:] == 1).all() - with pytest.raises(ValueError): - z2[:] = 3 - - -def test_open_with_mode_r_plus(tmp_path: pathlib.Path) -> None: - # 'r+' means read/write (must exist) - with pytest.raises(FileNotFoundError): - zarr.open(store=tmp_path, mode="r+") - zarr.ones(store=tmp_path, shape=(3, 3)) - z2 = zarr.open(store=tmp_path, mode="r+") - assert isinstance(z2, Array) - assert (z2[:] == 1).all() - z2[:] = 3 - - -async def test_open_with_mode_a(tmp_path: pathlib.Path) -> None: - # Open without shape argument should default to group - g = zarr.open(store=tmp_path, mode="a") - assert isinstance(g, Group) - await g.store_path.delete() - - # 'a' means read/write (create if doesn't exist) - arr = zarr.open(store=tmp_path, mode="a", shape=(3, 3)) - assert isinstance(arr, Array) - arr[...] = 1 - z2 = zarr.open(store=tmp_path, mode="a") - assert isinstance(z2, Array) - assert (z2[:] == 1).all() - z2[:] = 3 - - -def test_open_with_mode_w(tmp_path: pathlib.Path) -> None: - # 'w' means create (overwrite if exists); - arr = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) - assert isinstance(arr, Array) - - arr[...] = 3 - z2 = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) - assert isinstance(z2, Array) - assert not (z2[:] == 3).all() - z2[:] = 3 - - -def test_open_with_mode_w_minus(tmp_path: pathlib.Path) -> None: - # 'w-' means create (fail if exists) - arr = zarr.open(store=tmp_path, mode="w-", shape=(3, 3)) - assert isinstance(arr, Array) - arr[...] = 1 - with pytest.raises(FileExistsError): - zarr.open(store=tmp_path, mode="w-") - - -# def test_lazy_loader(): -# foo = np.arange(100) -# bar = np.arange(100, 0, -1) -# store = "data/group.zarr" -# save(store, foo=foo, bar=bar) -# loader = load(store) -# assert "foo" in loader -# assert "bar" in loader -# assert "baz" not in loader -# assert len(loader) == 2 -# assert sorted(loader) == ["bar", "foo"] -# assert_array_equal(foo, loader["foo"]) -# assert_array_equal(bar, loader["bar"]) -# assert "LazyLoader: " in repr(loader) - - -def test_load_array(memory_store: Store) -> None: - store = memory_store - foo = np.arange(100) - bar = np.arange(100, 0, -1) - save(store, foo=foo, bar=bar) - - # can also load arrays directly into a numpy array - for array_name in ["foo", "bar"]: - array = load(store, path=array_name) - assert isinstance(array, np.ndarray) - if array_name == "foo": - assert_array_equal(foo, array) - else: - assert_array_equal(bar, array) - - -def test_tree() -> None: - g1 = zarr.group() - g1.create_group("foo") - g3 = g1.create_group("bar") - g3.create_group("baz") - g5 = g3.create_group("qux") - g5.create_array("baz", shape=100, chunks=10) - # TODO: complete after tree has been reimplemented - # assert repr(zarr.tree(g1)) == repr(g1.tree()) - # assert str(zarr.tree(g1)) == str(g1.tree()) - - -# @pytest.mark.parametrize("stores_from_path", [False, True]) -# @pytest.mark.parametrize( -# "with_chunk_store,listable", -# [(False, True), (True, True), (False, False)], -# ids=["default-listable", "with_chunk_store-listable", "default-unlistable"], -# ) -# def test_consolidate_metadata(with_chunk_store, listable, monkeypatch, stores_from_path): -# # setup initial data -# if stores_from_path: -# store = tempfile.mkdtemp() -# atexit.register(atexit_rmtree, store) -# if with_chunk_store: -# chunk_store = tempfile.mkdtemp() -# atexit.register(atexit_rmtree, chunk_store) -# else: -# chunk_store = None -# else: -# store = MemoryStore() -# chunk_store = MemoryStore() if with_chunk_store else None -# path = None -# z = group(store, chunk_store=chunk_store, path=path) - -# # Reload the actual store implementation in case str -# store_to_copy = z.store - -# z.create_group("g1") -# g2 = z.create_group("g2") -# g2.attrs["hello"] = "world" -# arr = g2.create_array("arr", shape=(20, 20), chunks=(5, 5), dtype="f8") -# assert 16 == arr.nchunks -# assert 0 == arr.nchunks_initialized -# arr.attrs["data"] = 1 -# arr[:] = 1.0 -# assert 16 == arr.nchunks_initialized - -# if stores_from_path: -# # get the actual store class for use with consolidate_metadata -# store_class = z._store -# else: -# store_class = store - -# # perform consolidation -# out = consolidate_metadata(store_class, path=path) -# assert isinstance(out, Group) -# assert ["g1", "g2"] == list(out) -# if not stores_from_path: -# assert isinstance(out._store, ConsolidatedMetadataStore) -# assert ".zmetadata" in store -# meta_keys = [ -# ".zgroup", -# "g1/.zgroup", -# "g2/.zgroup", -# "g2/.zattrs", -# "g2/arr/.zarray", -# "g2/arr/.zattrs", -# ] - -# for key in meta_keys: -# del store[key] - -# # https://github.com/zarr-developers/zarr-python/issues/993 -# # Make sure we can still open consolidated on an unlistable store: -# if not listable: -# fs_memory = pytest.importorskip("fsspec.implementations.memory") -# monkeypatch.setattr(fs_memory.MemoryFileSystem, "isdir", lambda x, y: False) -# monkeypatch.delattr(fs_memory.MemoryFileSystem, "ls") -# fs = fs_memory.MemoryFileSystem() -# store_to_open = FSStore("", fs=fs) -# # copy original store to new unlistable store -# store_to_open.update(store_to_copy) - -# else: -# store_to_open = store - -# # open consolidated -# z2 = open_consolidated(store_to_open, chunk_store=chunk_store, path=path) -# assert ["g1", "g2"] == list(z2) -# assert "world" == z2.g2.attrs["hello"] -# assert 1 == z2.g2.arr.attrs["data"] -# assert (z2.g2.arr[:] == 1.0).all() -# assert 16 == z2.g2.arr.nchunks -# if listable: -# assert 16 == z2.g2.arr.nchunks_initialized -# else: -# with pytest.raises(NotImplementedError): -# _ = z2.g2.arr.nchunks_initialized - -# if stores_from_path: -# # path string is note a BaseStore subclass so cannot be used to -# # initialize a ConsolidatedMetadataStore. - -# with pytest.raises(ValueError): -# cmd = ConsolidatedMetadataStore(store) -# else: -# # tests del/write on the store - -# cmd = ConsolidatedMetadataStore(store) -# with pytest.raises(PermissionError): -# del cmd[".zgroup"] -# with pytest.raises(PermissionError): -# cmd[".zgroup"] = None - -# # test getsize on the store -# assert isinstance(getsize(cmd), Integral) - -# # test new metadata are not writeable -# with pytest.raises(PermissionError): -# z2.create_group("g3") -# with pytest.raises(PermissionError): -# z2.create_dataset("spam", shape=42, chunks=7, dtype="i4") -# with pytest.raises(PermissionError): -# del z2["g2"] - -# # test consolidated metadata are not writeable -# with pytest.raises(PermissionError): -# z2.g2.attrs["hello"] = "universe" -# with pytest.raises(PermissionError): -# z2.g2.arr.attrs["foo"] = "bar" - -# # test the data are writeable -# z2.g2.arr[:] = 2 -# assert (z2.g2.arr[:] == 2).all() - -# # test invalid modes -# with pytest.raises(ValueError): -# open_consolidated(store, chunk_store=chunk_store, mode="a", path=path) -# with pytest.raises(ValueError): -# open_consolidated(store, chunk_store=chunk_store, mode="w", path=path) -# with pytest.raises(ValueError): -# open_consolidated(store, chunk_store=chunk_store, mode="w-", path=path) - -# # make sure keyword arguments are passed through without error -# open_consolidated( -# store, -# chunk_store=chunk_store, -# path=path, -# cache_attrs=True, -# synchronizer=None, -# ) - - -# @pytest.mark.parametrize( -# "options", -# ( -# {"dimension_separator": "/"}, -# {"dimension_separator": "."}, -# {"dimension_separator": None}, -# ), -# ) -# def test_save_array_separator(tmpdir, options): -# data = np.arange(6).reshape((3, 2)) -# url = tmpdir.join("test.zarr") -# save_array(url, data, **options) - - -# class TestCopyStore(unittest.TestCase): -# _version = 2 - -# def setUp(self): -# source = dict() -# source["foo"] = b"xxx" -# source["bar/baz"] = b"yyy" -# source["bar/qux"] = b"zzz" -# self.source = source - -# def _get_dest_store(self): -# return dict() - -# def test_no_paths(self): -# source = self.source -# dest = self._get_dest_store() -# copy_store(source, dest) -# assert len(source) == len(dest) -# for key in source: -# assert source[key] == dest[key] - -# def test_source_path(self): -# source = self.source -# # paths should be normalized -# for source_path in "bar", "bar/", "/bar", "/bar/": -# dest = self._get_dest_store() -# copy_store(source, dest, source_path=source_path) -# assert 2 == len(dest) -# for key in source: -# if key.startswith("bar/"): -# dest_key = key.split("bar/")[1] -# assert source[key] == dest[dest_key] -# else: -# assert key not in dest - -# def test_dest_path(self): -# source = self.source -# # paths should be normalized -# for dest_path in "new", "new/", "/new", "/new/": -# dest = self._get_dest_store() -# copy_store(source, dest, dest_path=dest_path) -# assert len(source) == len(dest) -# for key in source: -# if self._version == 3: -# dest_key = key[:10] + "new/" + key[10:] -# else: -# dest_key = "new/" + key -# assert source[key] == dest[dest_key] - -# def test_source_dest_path(self): -# source = self.source -# # paths should be normalized -# for source_path in "bar", "bar/", "/bar", "/bar/": -# for dest_path in "new", "new/", "/new", "/new/": -# dest = self._get_dest_store() -# copy_store(source, dest, source_path=source_path, dest_path=dest_path) -# assert 2 == len(dest) -# for key in source: -# if key.startswith("bar/"): -# dest_key = "new/" + key.split("bar/")[1] -# assert source[key] == dest[dest_key] -# else: -# assert key not in dest -# assert ("new/" + key) not in dest - -# def test_excludes_includes(self): -# source = self.source - -# # single excludes -# dest = self._get_dest_store() -# excludes = "f.*" -# copy_store(source, dest, excludes=excludes) -# assert len(dest) == 2 - -# root = "" -# assert root + "foo" not in dest - -# # multiple excludes -# dest = self._get_dest_store() -# excludes = "b.z", ".*x" -# copy_store(source, dest, excludes=excludes) -# assert len(dest) == 1 -# assert root + "foo" in dest -# assert root + "bar/baz" not in dest -# assert root + "bar/qux" not in dest - -# # excludes and includes -# dest = self._get_dest_store() -# excludes = "b.*" -# includes = ".*x" -# copy_store(source, dest, excludes=excludes, includes=includes) -# assert len(dest) == 2 -# assert root + "foo" in dest -# assert root + "bar/baz" not in dest -# assert root + "bar/qux" in dest - -# def test_dry_run(self): -# source = self.source -# dest = self._get_dest_store() -# copy_store(source, dest, dry_run=True) -# assert 0 == len(dest) - -# def test_if_exists(self): -# source = self.source -# dest = self._get_dest_store() -# root = "" -# dest[root + "bar/baz"] = b"mmm" - -# # default ('raise') -# with pytest.raises(CopyError): -# copy_store(source, dest) - -# # explicit 'raise' -# with pytest.raises(CopyError): -# copy_store(source, dest, if_exists="raise") - -# # skip -# copy_store(source, dest, if_exists="skip") -# assert 3 == len(dest) -# assert dest[root + "foo"] == b"xxx" -# assert dest[root + "bar/baz"] == b"mmm" -# assert dest[root + "bar/qux"] == b"zzz" - -# # replace -# copy_store(source, dest, if_exists="replace") -# assert 3 == len(dest) -# assert dest[root + "foo"] == b"xxx" -# assert dest[root + "bar/baz"] == b"yyy" -# assert dest[root + "bar/qux"] == b"zzz" - -# # invalid option -# with pytest.raises(ValueError): -# copy_store(source, dest, if_exists="foobar") - - -# def check_copied_array(original, copied, without_attrs=False, expect_props=None): -# # setup -# source_h5py = original.__module__.startswith("h5py.") -# dest_h5py = copied.__module__.startswith("h5py.") -# zarr_to_zarr = not (source_h5py or dest_h5py) -# h5py_to_h5py = source_h5py and dest_h5py -# zarr_to_h5py = not source_h5py and dest_h5py -# h5py_to_zarr = source_h5py and not dest_h5py -# if expect_props is None: -# expect_props = dict() -# else: -# expect_props = expect_props.copy() - -# # common properties in zarr and h5py -# for p in "dtype", "shape", "chunks": -# expect_props.setdefault(p, getattr(original, p)) - -# # zarr-specific properties -# if zarr_to_zarr: -# for p in "compressor", "filters", "order", "fill_value": -# expect_props.setdefault(p, getattr(original, p)) - -# # h5py-specific properties -# if h5py_to_h5py: -# for p in ( -# "maxshape", -# "compression", -# "compression_opts", -# "shuffle", -# "scaleoffset", -# "fletcher32", -# "fillvalue", -# ): -# expect_props.setdefault(p, getattr(original, p)) - -# # common properties with some name differences -# if h5py_to_zarr: -# expect_props.setdefault("fill_value", original.fillvalue) -# if zarr_to_h5py: -# expect_props.setdefault("fillvalue", original.fill_value) - -# # compare properties -# for k, v in expect_props.items(): -# assert v == getattr(copied, k) - -# # compare data -# assert_array_equal(original[:], copied[:]) - -# # compare attrs -# if without_attrs: -# for k in original.attrs.keys(): -# assert k not in copied.attrs -# else: -# if dest_h5py and "filters" in original.attrs: -# # special case in v3 (storing filters metadata under attributes) -# # we explicitly do not copy this info over to HDF5 -# original_attrs = original.attrs.asdict().copy() -# original_attrs.pop("filters") -# else: -# original_attrs = original.attrs -# assert sorted(original_attrs.items()) == sorted(copied.attrs.items()) - - -# def check_copied_group(original, copied, without_attrs=False, expect_props=None, shallow=False): -# # setup -# if expect_props is None: -# expect_props = dict() -# else: -# expect_props = expect_props.copy() - -# # compare children -# for k, v in original.items(): -# if hasattr(v, "shape"): -# assert k in copied -# check_copied_array(v, copied[k], without_attrs=without_attrs, expect_props=expect_props) -# elif shallow: -# assert k not in copied -# else: -# assert k in copied -# check_copied_group( -# v, -# copied[k], -# without_attrs=without_attrs, -# shallow=shallow, -# expect_props=expect_props, -# ) - -# # compare attrs -# if without_attrs: -# for k in original.attrs.keys(): -# assert k not in copied.attrs -# else: -# assert sorted(original.attrs.items()) == sorted(copied.attrs.items()) - - -# def test_copy_all(): -# """ -# https://github.com/zarr-developers/zarr-python/issues/269 - -# copy_all used to not copy attributes as `.keys()` does not return hidden `.zattrs`. - -# """ -# original_group = zarr.group(store=MemoryStore(), overwrite=True) -# original_group.attrs["info"] = "group attrs" -# original_subgroup = original_group.create_group("subgroup") -# original_subgroup.attrs["info"] = "sub attrs" - -# destination_group = zarr.group(store=MemoryStore(), overwrite=True) - -# # copy from memory to directory store -# copy_all( -# original_group, -# destination_group, -# dry_run=False, -# ) - -# assert "subgroup" in destination_group -# assert destination_group.attrs["info"] == "group attrs" -# assert destination_group.subgroup.attrs["info"] == "sub attrs" - - -# class TestCopy: -# @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) -# def source(self, request, tmpdir): -# def prep_source(source): -# foo = source.create_group("foo") -# foo.attrs["experiment"] = "weird science" -# baz = foo.create_dataset("bar/baz", data=np.arange(100), chunks=(50,)) -# baz.attrs["units"] = "metres" -# if request.param: -# extra_kws = dict( -# compression="gzip", -# compression_opts=3, -# fillvalue=84, -# shuffle=True, -# fletcher32=True, -# ) -# else: -# extra_kws = dict(compressor=Zlib(3), order="F", fill_value=42, filters=[Adler32()]) -# source.create_dataset( -# "spam", -# data=np.arange(100, 200).reshape(20, 5), -# chunks=(10, 2), -# dtype="i2", -# **extra_kws, -# ) -# return source - -# if request.param: -# h5py = pytest.importorskip("h5py") -# fn = tmpdir.join("source.h5") -# with h5py.File(str(fn), mode="w") as h5f: -# yield prep_source(h5f) -# else: -# yield prep_source(group()) - -# @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) -# def dest(self, request, tmpdir): -# if request.param: -# h5py = pytest.importorskip("h5py") -# fn = tmpdir.join("dest.h5") -# with h5py.File(str(fn), mode="w") as h5f: -# yield h5f -# else: -# yield group() - -# def test_copy_array(self, source, dest): -# # copy array with default options -# copy(source["foo/bar/baz"], dest) -# check_copied_array(source["foo/bar/baz"], dest["baz"]) -# copy(source["spam"], dest) -# check_copied_array(source["spam"], dest["spam"]) - -# def test_copy_bad_dest(self, source, dest): -# # try to copy to an array, dest must be a group -# dest = dest.create_dataset("eggs", shape=(100,)) -# with pytest.raises(ValueError): -# copy(source["foo/bar/baz"], dest) - -# def test_copy_array_name(self, source, dest): -# # copy array with name -# copy(source["foo/bar/baz"], dest, name="qux") -# assert "baz" not in dest -# check_copied_array(source["foo/bar/baz"], dest["qux"]) - -# def test_copy_array_create_options(self, source, dest): -# dest_h5py = dest.__module__.startswith("h5py.") - -# # copy array, provide creation options -# compressor = Zlib(9) -# create_kws = dict(chunks=(10,)) -# if dest_h5py: -# create_kws.update( -# compression="gzip", compression_opts=9, shuffle=True, fletcher32=True, fillvalue=42 -# ) -# else: -# create_kws.update(compressor=compressor, fill_value=42, order="F", filters=[Adler32()]) -# copy(source["foo/bar/baz"], dest, without_attrs=True, **create_kws) -# check_copied_array( -# source["foo/bar/baz"], dest["baz"], without_attrs=True, expect_props=create_kws -# ) - -# def test_copy_array_exists_array(self, source, dest): -# # copy array, dest array in the way -# dest.create_dataset("baz", shape=(10,)) - -# # raise -# with pytest.raises(CopyError): -# # should raise by default -# copy(source["foo/bar/baz"], dest) -# assert (10,) == dest["baz"].shape -# with pytest.raises(CopyError): -# copy(source["foo/bar/baz"], dest, if_exists="raise") -# assert (10,) == dest["baz"].shape - -# # skip -# copy(source["foo/bar/baz"], dest, if_exists="skip") -# assert (10,) == dest["baz"].shape - -# # replace -# copy(source["foo/bar/baz"], dest, if_exists="replace") -# check_copied_array(source["foo/bar/baz"], dest["baz"]) - -# # invalid option -# with pytest.raises(ValueError): -# copy(source["foo/bar/baz"], dest, if_exists="foobar") - -# def test_copy_array_exists_group(self, source, dest): -# # copy array, dest group in the way -# dest.create_group("baz") - -# # raise -# with pytest.raises(CopyError): -# copy(source["foo/bar/baz"], dest) -# assert not hasattr(dest["baz"], "shape") -# with pytest.raises(CopyError): -# copy(source["foo/bar/baz"], dest, if_exists="raise") -# assert not hasattr(dest["baz"], "shape") - -# # skip -# copy(source["foo/bar/baz"], dest, if_exists="skip") -# assert not hasattr(dest["baz"], "shape") - -# # replace -# copy(source["foo/bar/baz"], dest, if_exists="replace") -# check_copied_array(source["foo/bar/baz"], dest["baz"]) - -# def test_copy_array_skip_initialized(self, source, dest): -# dest_h5py = dest.__module__.startswith("h5py.") - -# dest.create_dataset("baz", shape=(100,), chunks=(10,), dtype="i8") -# assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) - -# if dest_h5py: -# with pytest.raises(ValueError): -# # not available with copy to h5py -# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") - -# else: -# # copy array, dest array exists but not yet initialized -# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") -# check_copied_array(source["foo/bar/baz"], dest["baz"]) - -# # copy array, dest array exists and initialized, will be skipped -# dest["baz"][:] = np.arange(100, 200) -# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") -# assert_array_equal(np.arange(100, 200), dest["baz"][:]) -# assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) - -# def test_copy_group(self, source, dest): -# # copy group, default options -# copy(source["foo"], dest) -# check_copied_group(source["foo"], dest["foo"]) - -# def test_copy_group_no_name(self, source, dest): -# with pytest.raises(TypeError): -# # need a name if copy root -# copy(source, dest) - -# copy(source, dest, name="root") -# check_copied_group(source, dest["root"]) - -# def test_copy_group_options(self, source, dest): -# # copy group, non-default options -# copy(source["foo"], dest, name="qux", without_attrs=True) -# assert "foo" not in dest -# check_copied_group(source["foo"], dest["qux"], without_attrs=True) - -# def test_copy_group_shallow(self, source, dest): -# # copy group, shallow -# copy(source, dest, name="eggs", shallow=True) -# check_copied_group(source, dest["eggs"], shallow=True) - -# def test_copy_group_exists_group(self, source, dest): -# # copy group, dest groups exist -# dest.create_group("foo/bar") -# copy(source["foo"], dest) -# check_copied_group(source["foo"], dest["foo"]) - -# def test_copy_group_exists_array(self, source, dest): -# # copy group, dest array in the way -# dest.create_dataset("foo/bar", shape=(10,)) - -# # raise -# with pytest.raises(CopyError): -# copy(source["foo"], dest) -# assert dest["foo/bar"].shape == (10,) -# with pytest.raises(CopyError): -# copy(source["foo"], dest, if_exists="raise") -# assert dest["foo/bar"].shape == (10,) - -# # skip -# copy(source["foo"], dest, if_exists="skip") -# assert dest["foo/bar"].shape == (10,) - -# # replace -# copy(source["foo"], dest, if_exists="replace") -# check_copied_group(source["foo"], dest["foo"]) - -# def test_copy_group_dry_run(self, source, dest): -# # dry run, empty destination -# n_copied, n_skipped, n_bytes_copied = copy( -# source["foo"], dest, dry_run=True, return_stats=True -# ) -# assert 0 == len(dest) -# assert 3 == n_copied -# assert 0 == n_skipped -# assert 0 == n_bytes_copied - -# # dry run, array exists in destination -# baz = np.arange(100, 200) -# dest.create_dataset("foo/bar/baz", data=baz) -# assert not np.all(source["foo/bar/baz"][:] == dest["foo/bar/baz"][:]) -# assert 1 == len(dest) - -# # raise -# with pytest.raises(CopyError): -# copy(source["foo"], dest, dry_run=True) -# assert 1 == len(dest) - -# # skip -# n_copied, n_skipped, n_bytes_copied = copy( -# source["foo"], dest, dry_run=True, if_exists="skip", return_stats=True -# ) -# assert 1 == len(dest) -# assert 2 == n_copied -# assert 1 == n_skipped -# assert 0 == n_bytes_copied -# assert_array_equal(baz, dest["foo/bar/baz"]) - -# # replace -# n_copied, n_skipped, n_bytes_copied = copy( -# source["foo"], dest, dry_run=True, if_exists="replace", return_stats=True -# ) -# assert 1 == len(dest) -# assert 3 == n_copied -# assert 0 == n_skipped -# assert 0 == n_bytes_copied -# assert_array_equal(baz, dest["foo/bar/baz"]) - -# def test_logging(self, source, dest, tmpdir): -# # callable log -# copy(source["foo"], dest, dry_run=True, log=print) - -# # file name -# fn = str(tmpdir.join("log_name")) -# copy(source["foo"], dest, dry_run=True, log=fn) - -# # file -# with tmpdir.join("log_file").open(mode="w") as f: -# copy(source["foo"], dest, dry_run=True, log=f) - -# # bad option -# with pytest.raises(TypeError): -# copy(source["foo"], dest, dry_run=True, log=True) - - -def test_open_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") - with pytest.warns(FutureWarning, match="pass"): - open(store, "w", shape=(1,)) - - -def test_save_array_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") - with warnings.catch_warnings(): - warnings.filterwarnings( - "ignore", message="zarr_version is deprecated", category=DeprecationWarning - ) - with pytest.warns(FutureWarning, match="pass"): - save_array( - store, - np.ones( - 1, - ), - 3, - ) - - -def test_group_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") - with pytest.warns(FutureWarning, match="pass"): - group(store, True) - - -def test_open_group_positional_args_deprecated() -> None: - store = MemoryStore({}, mode="w") - 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") - zarr.open_group(store, attributes={"key": "value"}) - - group = zarr.open(store) - assert isinstance(group, Group) - assert group.attrs == {"key": "value"} - - -async def test_open_falls_back_to_open_group_async() -> None: - # https://github.com/zarr-developers/zarr-python/issues/2309 - store = MemoryStore(mode="w") - await zarr.api.asynchronous.open_group(store, attributes={"key": "value"}) - - group = await zarr.api.asynchronous.open(store=store) - assert isinstance(group, zarr.core.group.AsyncGroup) - assert group.attrs == {"key": "value"} From 2963e5eb379e24032bf0da22638565066e84946e Mon Sep 17 00:00:00 2001 From: Tom Augspurger Date: Wed, 9 Oct 2024 20:37:35 -0500 Subject: [PATCH 6/6] more --- src/zarr/core/metadata/v3.py | 2 +- src/zarr/errors.py | 2 - tests/v3/test_api.py | 960 ++++++++++++++++++++++++++++++ tests/v3/test_metadata/test_v3.py | 8 +- 4 files changed, 967 insertions(+), 5 deletions(-) diff --git a/src/zarr/core/metadata/v3.py b/src/zarr/core/metadata/v3.py index 215b983b6e..12e26746df 100644 --- a/src/zarr/core/metadata/v3.py +++ b/src/zarr/core/metadata/v3.py @@ -44,7 +44,7 @@ def parse_zarr_format(data: object) -> Literal[3]: def parse_node_type_array(data: object) -> Literal["array"]: if data == "array": return "array" - raise NodeTypeValidationError("array", data) + raise NodeTypeValidationError("node_type", "array", data) def parse_codecs(data: object) -> tuple[Codec, ...]: diff --git a/src/zarr/errors.py b/src/zarr/errors.py index 57045fccd3..e6d416bcc6 100644 --- a/src/zarr/errors.py +++ b/src/zarr/errors.py @@ -39,8 +39,6 @@ class NodeTypeValidationError(MetadataValidationError): for example an 'array' node when we expected a 'group'. """ - _msg = "Invalid value. Expected '{}'. Got '{}'." - __all__ = [ "ContainsArrayAndGroupError", diff --git a/tests/v3/test_api.py b/tests/v3/test_api.py index e69de29bb2..0614185f68 100644 --- a/tests/v3/test_api.py +++ b/tests/v3/test_api.py @@ -0,0 +1,960 @@ +import pathlib +import warnings + +import numpy as np +import pytest +from numpy.testing import assert_array_equal + +import zarr +import zarr.api.asynchronous +import zarr.core.group +from zarr import Array, Group +from zarr.abc.store import Store +from zarr.api.synchronous import create, group, load, open, open_group, save, save_array, save_group +from zarr.core.common import ZarrFormat +from zarr.errors import MetadataValidationError +from zarr.storage.memory import MemoryStore + + +def test_create_array(memory_store: Store) -> None: + store = memory_store + + # create array + z = create(shape=100, store=store) + assert isinstance(z, Array) + assert z.shape == (100,) + + # create array, overwrite, specify chunk shape + z = create(shape=200, chunk_shape=20, store=store, overwrite=True) + assert isinstance(z, Array) + assert z.shape == (200,) + assert z.chunks == (20,) + + # create array, overwrite, specify chunk shape via chunks param + z = create(shape=400, chunks=40, store=store, overwrite=True) + assert isinstance(z, Array) + assert z.shape == (400,) + assert z.chunks == (40,) + + +async def test_open_array(memory_store: MemoryStore) -> None: + store = memory_store + + # open array, create if doesn't exist + z = open(store=store, shape=100) + assert isinstance(z, Array) + assert z.shape == (100,) + + # open array, overwrite + # store._store_dict = {} + store = MemoryStore(mode="w") + 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) + assert isinstance(z, Array) + assert z.shape == (200,) + assert z.read_only + + # path not found + with pytest.raises(FileNotFoundError): + open(store="doesnotexist", mode="r") + + +async def test_open_group(memory_store: MemoryStore) -> None: + store = memory_store + + # open group, create if doesn't exist + g = open_group(store=store) + g.create_group("foo") + assert isinstance(g, Group) + assert "foo" in g + + # open group, overwrite + # g = open_group(store=store) + # assert isinstance(g, Group) + # assert "foo" not in g + + # 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) + assert isinstance(g, Group) + # assert g.read_only + + +@pytest.mark.parametrize("zarr_format", [None, 2, 3]) +async def test_open_group_unspecified_version( + tmpdir: pathlib.Path, zarr_format: ZarrFormat +) -> None: + """regression test for https://github.com/zarr-developers/zarr-python/issues/2175""" + + # create a group with specified zarr format (could be 2, 3, or None) + _ = await zarr.api.asynchronous.open_group( + store=str(tmpdir), mode="w", zarr_format=zarr_format, attributes={"foo": "bar"} + ) + + # now open that group without specifying the format + g2 = await zarr.api.asynchronous.open_group(store=str(tmpdir), mode="r") + + assert g2.attrs == {"foo": "bar"} + + if zarr_format is not None: + assert g2.metadata.zarr_format == zarr_format + + +def test_save_errors() -> None: + with pytest.raises(ValueError): + # no arrays provided + save_group("data/group.zarr") + with pytest.raises(TypeError): + # no array provided + save_array("data/group.zarr") + with pytest.raises(ValueError): + # no arrays provided + save("data/group.zarr") + + +def test_open_with_mode_r(tmp_path: pathlib.Path) -> None: + # 'r' means read only (must exist) + with pytest.raises(FileNotFoundError): + zarr.open(store=tmp_path, mode="r") + z1 = zarr.ones(store=tmp_path, shape=(3, 3)) + assert z1.fill_value == 1 + z2 = zarr.open(store=tmp_path, mode="r") + assert isinstance(z2, Array) + assert z2.fill_value == 1 + assert (z2[:] == 1).all() + with pytest.raises(ValueError): + z2[:] = 3 + + +def test_open_with_mode_r_plus(tmp_path: pathlib.Path) -> None: + # 'r+' means read/write (must exist) + with pytest.raises(FileNotFoundError): + zarr.open(store=tmp_path, mode="r+") + zarr.ones(store=tmp_path, shape=(3, 3)) + z2 = zarr.open(store=tmp_path, mode="r+") + assert isinstance(z2, Array) + assert (z2[:] == 1).all() + z2[:] = 3 + + +async def test_open_with_mode_a(tmp_path: pathlib.Path) -> None: + # Open without shape argument should default to group + g = zarr.open(store=tmp_path, mode="a") + assert isinstance(g, Group) + await g.store_path.delete() + + # 'a' means read/write (create if doesn't exist) + arr = zarr.open(store=tmp_path, mode="a", shape=(3, 3)) + assert isinstance(arr, Array) + arr[...] = 1 + z2 = zarr.open(store=tmp_path, mode="a") + assert isinstance(z2, Array) + assert (z2[:] == 1).all() + z2[:] = 3 + + +def test_open_with_mode_w(tmp_path: pathlib.Path) -> None: + # 'w' means create (overwrite if exists); + arr = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) + assert isinstance(arr, Array) + + arr[...] = 3 + z2 = zarr.open(store=tmp_path, mode="w", shape=(3, 3)) + assert isinstance(z2, Array) + assert not (z2[:] == 3).all() + z2[:] = 3 + + +def test_open_with_mode_w_minus(tmp_path: pathlib.Path) -> None: + # 'w-' means create (fail if exists) + arr = zarr.open(store=tmp_path, mode="w-", shape=(3, 3)) + assert isinstance(arr, Array) + arr[...] = 1 + with pytest.raises(FileExistsError): + zarr.open(store=tmp_path, mode="w-") + + +# def test_lazy_loader(): +# foo = np.arange(100) +# bar = np.arange(100, 0, -1) +# store = "data/group.zarr" +# save(store, foo=foo, bar=bar) +# loader = load(store) +# assert "foo" in loader +# assert "bar" in loader +# assert "baz" not in loader +# assert len(loader) == 2 +# assert sorted(loader) == ["bar", "foo"] +# assert_array_equal(foo, loader["foo"]) +# assert_array_equal(bar, loader["bar"]) +# assert "LazyLoader: " in repr(loader) + + +def test_load_array(memory_store: Store) -> None: + store = memory_store + foo = np.arange(100) + bar = np.arange(100, 0, -1) + save(store, foo=foo, bar=bar) + + # can also load arrays directly into a numpy array + for array_name in ["foo", "bar"]: + array = load(store, path=array_name) + assert isinstance(array, np.ndarray) + if array_name == "foo": + assert_array_equal(foo, array) + else: + assert_array_equal(bar, array) + + +def test_tree() -> None: + g1 = zarr.group() + g1.create_group("foo") + g3 = g1.create_group("bar") + g3.create_group("baz") + g5 = g3.create_group("qux") + g5.create_array("baz", shape=100, chunks=10) + # TODO: complete after tree has been reimplemented + # assert repr(zarr.tree(g1)) == repr(g1.tree()) + # assert str(zarr.tree(g1)) == str(g1.tree()) + + +# @pytest.mark.parametrize("stores_from_path", [False, True]) +# @pytest.mark.parametrize( +# "with_chunk_store,listable", +# [(False, True), (True, True), (False, False)], +# ids=["default-listable", "with_chunk_store-listable", "default-unlistable"], +# ) +# def test_consolidate_metadata(with_chunk_store, listable, monkeypatch, stores_from_path): +# # setup initial data +# if stores_from_path: +# store = tempfile.mkdtemp() +# atexit.register(atexit_rmtree, store) +# if with_chunk_store: +# chunk_store = tempfile.mkdtemp() +# atexit.register(atexit_rmtree, chunk_store) +# else: +# chunk_store = None +# else: +# store = MemoryStore() +# chunk_store = MemoryStore() if with_chunk_store else None +# path = None +# z = group(store, chunk_store=chunk_store, path=path) + +# # Reload the actual store implementation in case str +# store_to_copy = z.store + +# z.create_group("g1") +# g2 = z.create_group("g2") +# g2.attrs["hello"] = "world" +# arr = g2.create_array("arr", shape=(20, 20), chunks=(5, 5), dtype="f8") +# assert 16 == arr.nchunks +# assert 0 == arr.nchunks_initialized +# arr.attrs["data"] = 1 +# arr[:] = 1.0 +# assert 16 == arr.nchunks_initialized + +# if stores_from_path: +# # get the actual store class for use with consolidate_metadata +# store_class = z._store +# else: +# store_class = store + +# # perform consolidation +# out = consolidate_metadata(store_class, path=path) +# assert isinstance(out, Group) +# assert ["g1", "g2"] == list(out) +# if not stores_from_path: +# assert isinstance(out._store, ConsolidatedMetadataStore) +# assert ".zmetadata" in store +# meta_keys = [ +# ".zgroup", +# "g1/.zgroup", +# "g2/.zgroup", +# "g2/.zattrs", +# "g2/arr/.zarray", +# "g2/arr/.zattrs", +# ] + +# for key in meta_keys: +# del store[key] + +# # https://github.com/zarr-developers/zarr-python/issues/993 +# # Make sure we can still open consolidated on an unlistable store: +# if not listable: +# fs_memory = pytest.importorskip("fsspec.implementations.memory") +# monkeypatch.setattr(fs_memory.MemoryFileSystem, "isdir", lambda x, y: False) +# monkeypatch.delattr(fs_memory.MemoryFileSystem, "ls") +# fs = fs_memory.MemoryFileSystem() +# store_to_open = FSStore("", fs=fs) +# # copy original store to new unlistable store +# store_to_open.update(store_to_copy) + +# else: +# store_to_open = store + +# # open consolidated +# z2 = open_consolidated(store_to_open, chunk_store=chunk_store, path=path) +# assert ["g1", "g2"] == list(z2) +# assert "world" == z2.g2.attrs["hello"] +# assert 1 == z2.g2.arr.attrs["data"] +# assert (z2.g2.arr[:] == 1.0).all() +# assert 16 == z2.g2.arr.nchunks +# if listable: +# assert 16 == z2.g2.arr.nchunks_initialized +# else: +# with pytest.raises(NotImplementedError): +# _ = z2.g2.arr.nchunks_initialized + +# if stores_from_path: +# # path string is note a BaseStore subclass so cannot be used to +# # initialize a ConsolidatedMetadataStore. + +# with pytest.raises(ValueError): +# cmd = ConsolidatedMetadataStore(store) +# else: +# # tests del/write on the store + +# cmd = ConsolidatedMetadataStore(store) +# with pytest.raises(PermissionError): +# del cmd[".zgroup"] +# with pytest.raises(PermissionError): +# cmd[".zgroup"] = None + +# # test getsize on the store +# assert isinstance(getsize(cmd), Integral) + +# # test new metadata are not writeable +# with pytest.raises(PermissionError): +# z2.create_group("g3") +# with pytest.raises(PermissionError): +# z2.create_dataset("spam", shape=42, chunks=7, dtype="i4") +# with pytest.raises(PermissionError): +# del z2["g2"] + +# # test consolidated metadata are not writeable +# with pytest.raises(PermissionError): +# z2.g2.attrs["hello"] = "universe" +# with pytest.raises(PermissionError): +# z2.g2.arr.attrs["foo"] = "bar" + +# # test the data are writeable +# z2.g2.arr[:] = 2 +# assert (z2.g2.arr[:] == 2).all() + +# # test invalid modes +# with pytest.raises(ValueError): +# open_consolidated(store, chunk_store=chunk_store, mode="a", path=path) +# with pytest.raises(ValueError): +# open_consolidated(store, chunk_store=chunk_store, mode="w", path=path) +# with pytest.raises(ValueError): +# open_consolidated(store, chunk_store=chunk_store, mode="w-", path=path) + +# # make sure keyword arguments are passed through without error +# open_consolidated( +# store, +# chunk_store=chunk_store, +# path=path, +# cache_attrs=True, +# synchronizer=None, +# ) + + +# @pytest.mark.parametrize( +# "options", +# ( +# {"dimension_separator": "/"}, +# {"dimension_separator": "."}, +# {"dimension_separator": None}, +# ), +# ) +# def test_save_array_separator(tmpdir, options): +# data = np.arange(6).reshape((3, 2)) +# url = tmpdir.join("test.zarr") +# save_array(url, data, **options) + + +# class TestCopyStore(unittest.TestCase): +# _version = 2 + +# def setUp(self): +# source = dict() +# source["foo"] = b"xxx" +# source["bar/baz"] = b"yyy" +# source["bar/qux"] = b"zzz" +# self.source = source + +# def _get_dest_store(self): +# return dict() + +# def test_no_paths(self): +# source = self.source +# dest = self._get_dest_store() +# copy_store(source, dest) +# assert len(source) == len(dest) +# for key in source: +# assert source[key] == dest[key] + +# def test_source_path(self): +# source = self.source +# # paths should be normalized +# for source_path in "bar", "bar/", "/bar", "/bar/": +# dest = self._get_dest_store() +# copy_store(source, dest, source_path=source_path) +# assert 2 == len(dest) +# for key in source: +# if key.startswith("bar/"): +# dest_key = key.split("bar/")[1] +# assert source[key] == dest[dest_key] +# else: +# assert key not in dest + +# def test_dest_path(self): +# source = self.source +# # paths should be normalized +# for dest_path in "new", "new/", "/new", "/new/": +# dest = self._get_dest_store() +# copy_store(source, dest, dest_path=dest_path) +# assert len(source) == len(dest) +# for key in source: +# if self._version == 3: +# dest_key = key[:10] + "new/" + key[10:] +# else: +# dest_key = "new/" + key +# assert source[key] == dest[dest_key] + +# def test_source_dest_path(self): +# source = self.source +# # paths should be normalized +# for source_path in "bar", "bar/", "/bar", "/bar/": +# for dest_path in "new", "new/", "/new", "/new/": +# dest = self._get_dest_store() +# copy_store(source, dest, source_path=source_path, dest_path=dest_path) +# assert 2 == len(dest) +# for key in source: +# if key.startswith("bar/"): +# dest_key = "new/" + key.split("bar/")[1] +# assert source[key] == dest[dest_key] +# else: +# assert key not in dest +# assert ("new/" + key) not in dest + +# def test_excludes_includes(self): +# source = self.source + +# # single excludes +# dest = self._get_dest_store() +# excludes = "f.*" +# copy_store(source, dest, excludes=excludes) +# assert len(dest) == 2 + +# root = "" +# assert root + "foo" not in dest + +# # multiple excludes +# dest = self._get_dest_store() +# excludes = "b.z", ".*x" +# copy_store(source, dest, excludes=excludes) +# assert len(dest) == 1 +# assert root + "foo" in dest +# assert root + "bar/baz" not in dest +# assert root + "bar/qux" not in dest + +# # excludes and includes +# dest = self._get_dest_store() +# excludes = "b.*" +# includes = ".*x" +# copy_store(source, dest, excludes=excludes, includes=includes) +# assert len(dest) == 2 +# assert root + "foo" in dest +# assert root + "bar/baz" not in dest +# assert root + "bar/qux" in dest + +# def test_dry_run(self): +# source = self.source +# dest = self._get_dest_store() +# copy_store(source, dest, dry_run=True) +# assert 0 == len(dest) + +# def test_if_exists(self): +# source = self.source +# dest = self._get_dest_store() +# root = "" +# dest[root + "bar/baz"] = b"mmm" + +# # default ('raise') +# with pytest.raises(CopyError): +# copy_store(source, dest) + +# # explicit 'raise' +# with pytest.raises(CopyError): +# copy_store(source, dest, if_exists="raise") + +# # skip +# copy_store(source, dest, if_exists="skip") +# assert 3 == len(dest) +# assert dest[root + "foo"] == b"xxx" +# assert dest[root + "bar/baz"] == b"mmm" +# assert dest[root + "bar/qux"] == b"zzz" + +# # replace +# copy_store(source, dest, if_exists="replace") +# assert 3 == len(dest) +# assert dest[root + "foo"] == b"xxx" +# assert dest[root + "bar/baz"] == b"yyy" +# assert dest[root + "bar/qux"] == b"zzz" + +# # invalid option +# with pytest.raises(ValueError): +# copy_store(source, dest, if_exists="foobar") + + +# def check_copied_array(original, copied, without_attrs=False, expect_props=None): +# # setup +# source_h5py = original.__module__.startswith("h5py.") +# dest_h5py = copied.__module__.startswith("h5py.") +# zarr_to_zarr = not (source_h5py or dest_h5py) +# h5py_to_h5py = source_h5py and dest_h5py +# zarr_to_h5py = not source_h5py and dest_h5py +# h5py_to_zarr = source_h5py and not dest_h5py +# if expect_props is None: +# expect_props = dict() +# else: +# expect_props = expect_props.copy() + +# # common properties in zarr and h5py +# for p in "dtype", "shape", "chunks": +# expect_props.setdefault(p, getattr(original, p)) + +# # zarr-specific properties +# if zarr_to_zarr: +# for p in "compressor", "filters", "order", "fill_value": +# expect_props.setdefault(p, getattr(original, p)) + +# # h5py-specific properties +# if h5py_to_h5py: +# for p in ( +# "maxshape", +# "compression", +# "compression_opts", +# "shuffle", +# "scaleoffset", +# "fletcher32", +# "fillvalue", +# ): +# expect_props.setdefault(p, getattr(original, p)) + +# # common properties with some name differences +# if h5py_to_zarr: +# expect_props.setdefault("fill_value", original.fillvalue) +# if zarr_to_h5py: +# expect_props.setdefault("fillvalue", original.fill_value) + +# # compare properties +# for k, v in expect_props.items(): +# assert v == getattr(copied, k) + +# # compare data +# assert_array_equal(original[:], copied[:]) + +# # compare attrs +# if without_attrs: +# for k in original.attrs.keys(): +# assert k not in copied.attrs +# else: +# if dest_h5py and "filters" in original.attrs: +# # special case in v3 (storing filters metadata under attributes) +# # we explicitly do not copy this info over to HDF5 +# original_attrs = original.attrs.asdict().copy() +# original_attrs.pop("filters") +# else: +# original_attrs = original.attrs +# assert sorted(original_attrs.items()) == sorted(copied.attrs.items()) + + +# def check_copied_group(original, copied, without_attrs=False, expect_props=None, shallow=False): +# # setup +# if expect_props is None: +# expect_props = dict() +# else: +# expect_props = expect_props.copy() + +# # compare children +# for k, v in original.items(): +# if hasattr(v, "shape"): +# assert k in copied +# check_copied_array(v, copied[k], without_attrs=without_attrs, expect_props=expect_props) +# elif shallow: +# assert k not in copied +# else: +# assert k in copied +# check_copied_group( +# v, +# copied[k], +# without_attrs=without_attrs, +# shallow=shallow, +# expect_props=expect_props, +# ) + +# # compare attrs +# if without_attrs: +# for k in original.attrs.keys(): +# assert k not in copied.attrs +# else: +# assert sorted(original.attrs.items()) == sorted(copied.attrs.items()) + + +# def test_copy_all(): +# """ +# https://github.com/zarr-developers/zarr-python/issues/269 + +# copy_all used to not copy attributes as `.keys()` does not return hidden `.zattrs`. + +# """ +# original_group = zarr.group(store=MemoryStore(), overwrite=True) +# original_group.attrs["info"] = "group attrs" +# original_subgroup = original_group.create_group("subgroup") +# original_subgroup.attrs["info"] = "sub attrs" + +# destination_group = zarr.group(store=MemoryStore(), overwrite=True) + +# # copy from memory to directory store +# copy_all( +# original_group, +# destination_group, +# dry_run=False, +# ) + +# assert "subgroup" in destination_group +# assert destination_group.attrs["info"] == "group attrs" +# assert destination_group.subgroup.attrs["info"] == "sub attrs" + + +# class TestCopy: +# @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) +# def source(self, request, tmpdir): +# def prep_source(source): +# foo = source.create_group("foo") +# foo.attrs["experiment"] = "weird science" +# baz = foo.create_dataset("bar/baz", data=np.arange(100), chunks=(50,)) +# baz.attrs["units"] = "metres" +# if request.param: +# extra_kws = dict( +# compression="gzip", +# compression_opts=3, +# fillvalue=84, +# shuffle=True, +# fletcher32=True, +# ) +# else: +# extra_kws = dict(compressor=Zlib(3), order="F", fill_value=42, filters=[Adler32()]) +# source.create_dataset( +# "spam", +# data=np.arange(100, 200).reshape(20, 5), +# chunks=(10, 2), +# dtype="i2", +# **extra_kws, +# ) +# return source + +# if request.param: +# h5py = pytest.importorskip("h5py") +# fn = tmpdir.join("source.h5") +# with h5py.File(str(fn), mode="w") as h5f: +# yield prep_source(h5f) +# else: +# yield prep_source(group()) + +# @pytest.fixture(params=[False, True], ids=["zarr", "hdf5"]) +# def dest(self, request, tmpdir): +# if request.param: +# h5py = pytest.importorskip("h5py") +# fn = tmpdir.join("dest.h5") +# with h5py.File(str(fn), mode="w") as h5f: +# yield h5f +# else: +# yield group() + +# def test_copy_array(self, source, dest): +# # copy array with default options +# copy(source["foo/bar/baz"], dest) +# check_copied_array(source["foo/bar/baz"], dest["baz"]) +# copy(source["spam"], dest) +# check_copied_array(source["spam"], dest["spam"]) + +# def test_copy_bad_dest(self, source, dest): +# # try to copy to an array, dest must be a group +# dest = dest.create_dataset("eggs", shape=(100,)) +# with pytest.raises(ValueError): +# copy(source["foo/bar/baz"], dest) + +# def test_copy_array_name(self, source, dest): +# # copy array with name +# copy(source["foo/bar/baz"], dest, name="qux") +# assert "baz" not in dest +# check_copied_array(source["foo/bar/baz"], dest["qux"]) + +# def test_copy_array_create_options(self, source, dest): +# dest_h5py = dest.__module__.startswith("h5py.") + +# # copy array, provide creation options +# compressor = Zlib(9) +# create_kws = dict(chunks=(10,)) +# if dest_h5py: +# create_kws.update( +# compression="gzip", compression_opts=9, shuffle=True, fletcher32=True, fillvalue=42 +# ) +# else: +# create_kws.update(compressor=compressor, fill_value=42, order="F", filters=[Adler32()]) +# copy(source["foo/bar/baz"], dest, without_attrs=True, **create_kws) +# check_copied_array( +# source["foo/bar/baz"], dest["baz"], without_attrs=True, expect_props=create_kws +# ) + +# def test_copy_array_exists_array(self, source, dest): +# # copy array, dest array in the way +# dest.create_dataset("baz", shape=(10,)) + +# # raise +# with pytest.raises(CopyError): +# # should raise by default +# copy(source["foo/bar/baz"], dest) +# assert (10,) == dest["baz"].shape +# with pytest.raises(CopyError): +# copy(source["foo/bar/baz"], dest, if_exists="raise") +# assert (10,) == dest["baz"].shape + +# # skip +# copy(source["foo/bar/baz"], dest, if_exists="skip") +# assert (10,) == dest["baz"].shape + +# # replace +# copy(source["foo/bar/baz"], dest, if_exists="replace") +# check_copied_array(source["foo/bar/baz"], dest["baz"]) + +# # invalid option +# with pytest.raises(ValueError): +# copy(source["foo/bar/baz"], dest, if_exists="foobar") + +# def test_copy_array_exists_group(self, source, dest): +# # copy array, dest group in the way +# dest.create_group("baz") + +# # raise +# with pytest.raises(CopyError): +# copy(source["foo/bar/baz"], dest) +# assert not hasattr(dest["baz"], "shape") +# with pytest.raises(CopyError): +# copy(source["foo/bar/baz"], dest, if_exists="raise") +# assert not hasattr(dest["baz"], "shape") + +# # skip +# copy(source["foo/bar/baz"], dest, if_exists="skip") +# assert not hasattr(dest["baz"], "shape") + +# # replace +# copy(source["foo/bar/baz"], dest, if_exists="replace") +# check_copied_array(source["foo/bar/baz"], dest["baz"]) + +# def test_copy_array_skip_initialized(self, source, dest): +# dest_h5py = dest.__module__.startswith("h5py.") + +# dest.create_dataset("baz", shape=(100,), chunks=(10,), dtype="i8") +# assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) + +# if dest_h5py: +# with pytest.raises(ValueError): +# # not available with copy to h5py +# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") + +# else: +# # copy array, dest array exists but not yet initialized +# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") +# check_copied_array(source["foo/bar/baz"], dest["baz"]) + +# # copy array, dest array exists and initialized, will be skipped +# dest["baz"][:] = np.arange(100, 200) +# copy(source["foo/bar/baz"], dest, if_exists="skip_initialized") +# assert_array_equal(np.arange(100, 200), dest["baz"][:]) +# assert not np.all(source["foo/bar/baz"][:] == dest["baz"][:]) + +# def test_copy_group(self, source, dest): +# # copy group, default options +# copy(source["foo"], dest) +# check_copied_group(source["foo"], dest["foo"]) + +# def test_copy_group_no_name(self, source, dest): +# with pytest.raises(TypeError): +# # need a name if copy root +# copy(source, dest) + +# copy(source, dest, name="root") +# check_copied_group(source, dest["root"]) + +# def test_copy_group_options(self, source, dest): +# # copy group, non-default options +# copy(source["foo"], dest, name="qux", without_attrs=True) +# assert "foo" not in dest +# check_copied_group(source["foo"], dest["qux"], without_attrs=True) + +# def test_copy_group_shallow(self, source, dest): +# # copy group, shallow +# copy(source, dest, name="eggs", shallow=True) +# check_copied_group(source, dest["eggs"], shallow=True) + +# def test_copy_group_exists_group(self, source, dest): +# # copy group, dest groups exist +# dest.create_group("foo/bar") +# copy(source["foo"], dest) +# check_copied_group(source["foo"], dest["foo"]) + +# def test_copy_group_exists_array(self, source, dest): +# # copy group, dest array in the way +# dest.create_dataset("foo/bar", shape=(10,)) + +# # raise +# with pytest.raises(CopyError): +# copy(source["foo"], dest) +# assert dest["foo/bar"].shape == (10,) +# with pytest.raises(CopyError): +# copy(source["foo"], dest, if_exists="raise") +# assert dest["foo/bar"].shape == (10,) + +# # skip +# copy(source["foo"], dest, if_exists="skip") +# assert dest["foo/bar"].shape == (10,) + +# # replace +# copy(source["foo"], dest, if_exists="replace") +# check_copied_group(source["foo"], dest["foo"]) + +# def test_copy_group_dry_run(self, source, dest): +# # dry run, empty destination +# n_copied, n_skipped, n_bytes_copied = copy( +# source["foo"], dest, dry_run=True, return_stats=True +# ) +# assert 0 == len(dest) +# assert 3 == n_copied +# assert 0 == n_skipped +# assert 0 == n_bytes_copied + +# # dry run, array exists in destination +# baz = np.arange(100, 200) +# dest.create_dataset("foo/bar/baz", data=baz) +# assert not np.all(source["foo/bar/baz"][:] == dest["foo/bar/baz"][:]) +# assert 1 == len(dest) + +# # raise +# with pytest.raises(CopyError): +# copy(source["foo"], dest, dry_run=True) +# assert 1 == len(dest) + +# # skip +# n_copied, n_skipped, n_bytes_copied = copy( +# source["foo"], dest, dry_run=True, if_exists="skip", return_stats=True +# ) +# assert 1 == len(dest) +# assert 2 == n_copied +# assert 1 == n_skipped +# assert 0 == n_bytes_copied +# assert_array_equal(baz, dest["foo/bar/baz"]) + +# # replace +# n_copied, n_skipped, n_bytes_copied = copy( +# source["foo"], dest, dry_run=True, if_exists="replace", return_stats=True +# ) +# assert 1 == len(dest) +# assert 3 == n_copied +# assert 0 == n_skipped +# assert 0 == n_bytes_copied +# assert_array_equal(baz, dest["foo/bar/baz"]) + +# def test_logging(self, source, dest, tmpdir): +# # callable log +# copy(source["foo"], dest, dry_run=True, log=print) + +# # file name +# fn = str(tmpdir.join("log_name")) +# copy(source["foo"], dest, dry_run=True, log=fn) + +# # file +# with tmpdir.join("log_file").open(mode="w") as f: +# copy(source["foo"], dest, dry_run=True, log=f) + +# # bad option +# with pytest.raises(TypeError): +# copy(source["foo"], dest, dry_run=True, log=True) + + +def test_open_positional_args_deprecated() -> None: + store = MemoryStore({}, mode="w") + with pytest.warns(FutureWarning, match="pass"): + open(store, "w", shape=(1,)) + + +def test_save_array_positional_args_deprecated() -> None: + store = MemoryStore({}, mode="w") + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message="zarr_version is deprecated", category=DeprecationWarning + ) + with pytest.warns(FutureWarning, match="pass"): + save_array( + store, + np.ones( + 1, + ), + 3, + ) + + +def test_group_positional_args_deprecated() -> None: + store = MemoryStore({}, mode="w") + with pytest.warns(FutureWarning, match="pass"): + group(store, True) + + +def test_open_group_positional_args_deprecated() -> None: + store = MemoryStore({}, mode="w") + 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") + zarr.open_group(store, attributes={"key": "value"}) + + group = zarr.open(store) + assert isinstance(group, Group) + assert group.attrs == {"key": "value"} + + +async def test_open_falls_back_to_open_group_async() -> None: + # https://github.com/zarr-developers/zarr-python/issues/2309 + store = MemoryStore(mode="w") + await zarr.api.asynchronous.open_group(store, attributes={"key": "value"}) + + group = await zarr.api.asynchronous.open(store=store) + assert isinstance(group, zarr.core.group.AsyncGroup) + assert group.attrs == {"key": "value"} + + +async def test_metadata_validation_error() -> None: + with pytest.raises( + MetadataValidationError, + match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.", + ): + await zarr.api.asynchronous.open_group(zarr_format="3.0") # type: ignore[arg-type] + + with pytest.raises( + MetadataValidationError, + match="Invalid value for 'zarr_format'. Expected '2, 3, or None'. Got '3.0'.", + ): + await zarr.api.asynchronous.open_array(shape=(1,), zarr_format="3.0") # type: ignore[arg-type] diff --git a/tests/v3/test_metadata/test_v3.py b/tests/v3/test_metadata/test_v3.py index 6d433b5f0e..f3b2968680 100644 --- a/tests/v3/test_metadata/test_v3.py +++ b/tests/v3/test_metadata/test_v3.py @@ -55,7 +55,9 @@ @pytest.mark.parametrize("data", [None, 1, 2, 4, 5, "3"]) def test_parse_zarr_format_invalid(data: Any) -> None: - with pytest.raises(ValueError, match=f"Invalid value. Expected '3'. Got '{data}'."): + with pytest.raises( + ValueError, match=f"Invalid value for 'zarr_format'. Expected '3'. Got '{data}'." + ): parse_zarr_format(data) @@ -65,7 +67,9 @@ def test_parse_zarr_format_valid() -> None: @pytest.mark.parametrize("data", [None, "group"]) def test_parse_node_type_arrayinvalid(data: Any) -> None: - with pytest.raises(ValueError, match=f"Invalid value. Expected 'array'. Got '{data}'."): + with pytest.raises( + ValueError, match=f"Invalid value for 'node_type'. Expected 'array'. Got '{data}'." + ): parse_node_type_array(data)