Skip to content

Commit f6a08a0

Browse files
committed
improve coverage with more tests
1 parent 02ac91d commit f6a08a0

File tree

2 files changed

+108
-20
lines changed

2 files changed

+108
-20
lines changed

src/zarr/core/group.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from collections.abc import (
7373
AsyncGenerator,
7474
AsyncIterator,
75+
Callable,
7576
Coroutine,
7677
Generator,
7778
Iterable,
@@ -2898,10 +2899,17 @@ async def create_hierarchy(
28982899
implicit_group_names = set(nodes_parsed.keys()) - set(nodes.keys())
28992900

29002901
zarr_format = sample.zarr_format
2901-
if zarr_format == 3 or zarr_format == 2:
2902+
# TODO: this type hint is so long
2903+
func: (
2904+
Callable[[StorePath], Coroutine[Any, Any, GroupMetadata | ArrayV3Metadata]]
2905+
| Callable[[StorePath], Coroutine[Any, Any, GroupMetadata | ArrayV2Metadata]]
2906+
)
2907+
if zarr_format == 3:
29022908
func = _read_metadata_v3
2903-
else:
2904-
raise ValueError(f"Invalid zarr_format: {zarr_format}")
2909+
elif zarr_format == 2:
2910+
func = _read_metadata_v2
2911+
else: # pragma: no cover
2912+
raise ValueError(f"Invalid zarr_format: {zarr_format}") # pragma: no cover
29052913

29062914
coros = (func(store_path=store_path / key) for key in nodes_parsed)
29072915
extant_node_query = dict(
@@ -3319,7 +3327,7 @@ def _build_metadata_v3(zarr_json: dict[str, JSON]) -> ArrayV3Metadata | GroupMet
33193327
Convert a dict representation of Zarr V3 metadata into the corresponding metadata class.
33203328
"""
33213329
if "node_type" not in zarr_json:
3322-
raise KeyError("missing `node_type` key in metadata document.")
3330+
raise MetadataValidationError("node_type", "array or group", "nothing (the key is missing)")
33233331
match zarr_json:
33243332
case {"node_type": "array"}:
33253333
return ArrayV3Metadata.from_dict(zarr_json)
@@ -3395,7 +3403,7 @@ async def _read_node(
33953403
store_path: StorePath, zarr_format: ZarrFormat
33963404
) -> AsyncArray[ArrayV2Metadata] | AsyncArray[ArrayV3Metadata] | AsyncGroup:
33973405
"""
3398-
Read and AsyncArray or AsyncGroup from a location defined by a StorePath.
3406+
Read an AsyncArray or AsyncGroup from a location defined by a StorePath.
33993407
"""
34003408
match zarr_format:
34013409
case 2:

tests/test_group.py

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from zarr.core.group import (
2525
ConsolidatedMetadata,
2626
GroupMetadata,
27+
_build_metadata_v3,
2728
_create_rooted_hierarchy,
2829
_create_rooted_hierarchy_sync,
2930
_join_paths,
@@ -34,8 +35,9 @@
3435
create_hierarchy,
3536
create_nodes,
3637
)
38+
from zarr.core.metadata.v3 import ArrayV3Metadata
3739
from zarr.core.sync import _collect_aiterator, sync
38-
from zarr.errors import ContainsArrayError, ContainsGroupError
40+
from zarr.errors import ContainsArrayError, ContainsGroupError, MetadataValidationError
3941
from zarr.storage import LocalStore, MemoryStore, StorePath, ZipStore
4042
from zarr.storage._common import make_store_path
4143
from zarr.storage._utils import normalize_path
@@ -1516,9 +1518,53 @@ async def test_create_hierarchy(store: Store, overwrite: bool, zarr_format: Zarr
15161518
assert expected_meta == {k: v.metadata for k, v in observed_nodes.items()}
15171519

15181520

1521+
@pytest.mark.parametrize("store", ["memory"], indirect=True)
1522+
@pytest.mark.parametrize("extant_node", ["array", "group"])
1523+
async def test_create_hierarchy_existing_nodes(
1524+
store: Store, extant_node: Literal["array", "group"], zarr_format: ZarrFormat
1525+
) -> None:
1526+
"""
1527+
Test that create_hierarchy with overwrite = False will not overwrite an existing array or group,
1528+
and raises an exception instead.
1529+
"""
1530+
spath = await make_store_path(store, path="path")
1531+
extant_node_path = "node"
1532+
1533+
if extant_node == "array":
1534+
extant_metadata = meta_from_array(
1535+
np.zeros(4), zarr_format=zarr_format, attributes={"extant": True}
1536+
)
1537+
new_metadata = meta_from_array(np.zeros(4), zarr_format=zarr_format)
1538+
err_cls = ContainsArrayError
1539+
else:
1540+
extant_metadata = GroupMetadata(zarr_format=zarr_format, attributes={"extant": True})
1541+
new_metadata = GroupMetadata(zarr_format=zarr_format)
1542+
err_cls = ContainsGroupError
1543+
1544+
# write the extant metadata
1545+
sync(
1546+
_collect_aiterator(
1547+
create_nodes(store_path=spath, nodes={extant_node_path: extant_metadata})
1548+
)
1549+
)
1550+
1551+
msg = f"{extant_node} exists in store {store!r} at path {extant_node_path!r}."
1552+
# ensure that we cannot invoke create_hierarchy with overwrite=False here
1553+
with pytest.raises(err_cls, match=re.escape(msg)):
1554+
sync(
1555+
_collect_aiterator(
1556+
create_hierarchy(store_path=spath, nodes={"node": new_metadata}, overwrite=False)
1557+
)
1558+
)
1559+
# ensure that the extant metadata was not overwritten
1560+
assert (
1561+
await _read_node(spath / extant_node_path, zarr_format=zarr_format)
1562+
).metadata.attributes == {"extant": True}
1563+
1564+
15191565
@pytest.mark.parametrize("store", ["memory"], indirect=True)
15201566
@pytest.mark.parametrize("overwrite", [True, False])
1521-
def test_group_create_hierarchy(store: Store, zarr_format: ZarrFormat, overwrite: bool):
1567+
def test_group_create_hierarchy(store: Store, zarr_format: ZarrFormat, overwrite: bool) -> None:
15221568
"""
15231569
Test that the Group.create_hierarchy method creates specified nodes and returns them in a dict.
15241570
"""
@@ -1537,7 +1583,9 @@ def test_group_create_hierarchy(store: Store, zarr_format: ZarrFormat, overwrite
15371583

15381584
@pytest.mark.parametrize("store", ["memory"], indirect=True)
15391585
@pytest.mark.parametrize("overwrite", [True, False])
1540-
def test_group_create_hierarchy_no_root(store: Store, zarr_format: ZarrFormat, overwrite: bool):
1586+
def test_group_create_hierarchy_no_root(
1587+
store: Store, zarr_format: ZarrFormat, overwrite: bool
1588+
) -> None:
15411589
"""
15421590
Test that the Group.create_hierarchy method will error if the dict provided contains a root.
15431591
"""
@@ -1552,7 +1600,9 @@ def test_group_create_hierarchy_no_root(store: Store, zarr_format: ZarrFormat, o
15521600

15531601

15541602
@pytest.mark.parametrize("store", ["memory"], indirect=True)
1555-
def test_group_create_hierarchy_invalid_mixed_zarr_format(store: Store, zarr_format: ZarrFormat):
1603+
def test_group_create_hierarchy_invalid_mixed_zarr_format(
1604+
store: Store, zarr_format: ZarrFormat
1605+
) -> None:
15561606
"""
15571607
Test that ``Group.create_hierarchy`` will raise an error if the zarr_format of the nodes is
15581608
different from the parent group.
@@ -1597,7 +1647,7 @@ async def test_create_hierarchy_invalid_nested(
15971647

15981648

15991649
@pytest.mark.parametrize("store", ["memory"], indirect=True)
1600-
async def test_create_hierarchy_invalid_mixed_format(store: Store):
1650+
async def test_create_hierarchy_invalid_mixed_format(store: Store) -> None:
16011651
"""
16021652
Test that create_hierarchy will not create a Zarr group that contains a both Zarr v2 and
16031653
Zarr v3 nodes.
@@ -1625,7 +1675,9 @@ async def test_create_hierarchy_invalid_mixed_format(store: Store):
16251675
@pytest.mark.parametrize("zarr_format", [2, 3])
16261676
@pytest.mark.parametrize("root_key", ["", "root"])
16271677
@pytest.mark.parametrize("path", ["", "foo"])
1628-
async def test_create_rooted_hierarchy_group(store: Store, zarr_format, path: str, root_key: str):
1678+
async def test_create_rooted_hierarchy_group(
1679+
store: Store, zarr_format, path: str, root_key: str
1680+
) -> None:
16291681
"""
16301682
Test that the _create_rooted_hierarchy can create a group.
16311683
"""
@@ -1663,8 +1715,8 @@ async def test_create_rooted_hierarchy_group(store: Store, zarr_format, path: st
16631715
assert members_observed_meta == members_expected_meta_relative
16641716

16651717

1666-
@pytest.mark.parametrize("store", ["memory", "local"], indirect=True)
1667-
def test_create_hierarchy_implicit_groups(store: Store):
1718+
@pytest.mark.parametrize("store", ["memory"], indirect=True)
1719+
def test_create_hierarchy_implicit_groups(store: Store) -> None:
16681720
spath = sync(make_store_path(store, path=""))
16691721
nodes = {
16701722
"": GroupMetadata(zarr_format=3, attributes={"implicit": False}),
@@ -1682,7 +1734,9 @@ def test_create_hierarchy_implicit_groups(store: Store):
16821734
@pytest.mark.parametrize("zarr_format", [2, 3])
16831735
@pytest.mark.parametrize("root_key", ["", "root"])
16841736
@pytest.mark.parametrize("path", ["", "foo"])
1685-
def test_create_rooted_hierarchy_sync_group(store: Store, zarr_format, path: str, root_key: str):
1737+
def test_create_rooted_hierarchy_sync_group(
1738+
store: Store, zarr_format, path: str, root_key: str
1739+
) -> None:
16861740
"""
16871741
Test that the _create_rooted_hierarchy_sync can create a group.
16881742
"""
@@ -1723,7 +1777,9 @@ def test_create_rooted_hierarchy_sync_group(store: Store, zarr_format, path: str
17231777
@pytest.mark.parametrize("zarr_format", [2, 3])
17241778
@pytest.mark.parametrize("root_key", ["", "root"])
17251779
@pytest.mark.parametrize("path", ["", "foo"])
1726-
async def test_create_rooted_hierarchy_array(store: Store, zarr_format, path: str, root_key: str):
1780+
async def test_create_rooted_hierarchy_array(
1781+
store: Store, zarr_format, path: str, root_key: str
1782+
) -> None:
17271783
"""
17281784
Test that the _create_rooted_hierarchy can create an array.
17291785
"""
@@ -1747,7 +1803,7 @@ async def test_create_rooted_hierarchy_array(store: Store, zarr_format, path: st
17471803
@pytest.mark.parametrize("path", ["", "foo"])
17481804
async def test_create_rooted_hierarchy_sync_array(
17491805
store: Store, zarr_format, path: str, root_key: str
1750-
):
1806+
) -> None:
17511807
"""
17521808
Test that _create_rooted_hierarchy_sync can create an array.
17531809
"""
@@ -1765,7 +1821,7 @@ async def test_create_rooted_hierarchy_sync_array(
17651821
assert a.metadata.attributes == {"path": root_key}
17661822

17671823

1768-
async def test_create_rooted_hierarchy_invalid():
1824+
async def test_create_rooted_hierarchy_invalid() -> None:
17691825
"""
17701826
Ensure _create_rooted_hierarchy will raise a ValueError if the input does not contain
17711827
a root node.
@@ -1781,7 +1837,7 @@ async def test_create_rooted_hierarchy_invalid():
17811837

17821838

17831839
@pytest.mark.parametrize("paths", [("a", "/a"), ("", "/"), ("b/", "b")])
1784-
def test_normalize_paths_invalid(paths: tuple[str, str]):
1840+
def test_normalize_paths_invalid(paths: tuple[str, str]) -> None:
17851841
"""
17861842
Ensure that calling _normalize_paths on values that will normalize to the same value
17871843
will generate a ValueError.
@@ -1795,7 +1851,7 @@ def test_normalize_paths_invalid(paths: tuple[str, str]):
17951851
@pytest.mark.parametrize(
17961852
"paths", [("/a", "a/b"), ("a", "a/b"), ("a/", "a///b"), ("/a/", "//a/b///")]
17971853
)
1798-
def test_normalize_paths_valid(paths: tuple[str, str]):
1854+
def test_normalize_paths_valid(paths: tuple[str, str]) -> None:
17991855
"""
18001856
Ensure that calling _normalize_paths on values that normalize to distinct values
18011857
returns a tuple of those normalized values.
@@ -1804,7 +1860,10 @@ def test_normalize_paths_valid(paths: tuple[str, str]):
18041860
assert _normalize_paths(paths) == expected
18051861

18061862

1807-
def test_normalize_path_keys():
1863+
def test_normalize_path_keys() -> None:
1864+
"""
1865+
Test that normalize_path_keys returns a dict where each key has been normalized.
1866+
"""
18081867
data = {"": 10, "a": "hello", "a/b": None, "/a/b/c/d": None}
18091868
observed = _normalize_path_keys(data)
18101869
expected = {normalize_path(k): v for k, v in data.items()}
@@ -1874,3 +1933,24 @@ def test_group_members_concurrency_limit(store: MemoryStore) -> None:
18741933
elapsed = time.time() - start
18751934

18761935
assert elapsed > num_groups * get_latency
1936+
1937+
1938+
@pytest.mark.parametrize("option", ["array", "group", "invalid"])
1939+
def test_build_metadata_v3(option: Literal["array", "group", "invalid"]) -> None:
1940+
"""
1941+
Test that _build_metadata_v3 returns the correct metadata for a v3 array or group
1942+
"""
1943+
match option:
1944+
case "array":
1945+
metadata_dict = meta_from_array(np.arange(10), zarr_format=3).to_dict()
1946+
assert _build_metadata_v3(metadata_dict) == ArrayV3Metadata.from_dict(metadata_dict)
1947+
case "group":
1948+
metadata_dict = GroupMetadata(attributes={"foo": 10}, zarr_format=3).to_dict()
1949+
assert _build_metadata_v3(metadata_dict) == GroupMetadata.from_dict(metadata_dict)
1950+
case "invalid":
1951+
metadata_dict = GroupMetadata(zarr_format=3).to_dict()
1952+
metadata_dict.pop("node_type")
1953+
# TODO: fix the error message
1954+
msg = "Invalid value for 'node_type'. Expected 'array or group'. Got 'nothing (the key is missing)'."
1955+
with pytest.raises(MetadataValidationError, match=re.escape(msg)):
1956+
_build_metadata_v3(metadata_dict)

0 commit comments

Comments
 (0)