diff --git a/src/zarr/core/group.py b/src/zarr/core/group.py index a7f8a6c022..b16c130b54 100644 --- a/src/zarr/core/group.py +++ b/src/zarr/core/group.py @@ -3458,7 +3458,7 @@ def _build_metadata_v3(zarr_json: dict[str, JSON]) -> ArrayV3Metadata | GroupMet def _build_metadata_v2( - zarr_json: dict[str, object], attrs_json: dict[str, JSON] + zarr_json: dict[str, JSON], attrs_json: dict[str, JSON] ) -> ArrayV2Metadata | GroupMetadata: """ Convert a dict representation of Zarr V2 metadata into the corresponding metadata class. diff --git a/src/zarr/core/metadata/v2.py b/src/zarr/core/metadata/v2.py index a359e116f5..0e27e20175 100644 --- a/src/zarr/core/metadata/v2.py +++ b/src/zarr/core/metadata/v2.py @@ -318,7 +318,9 @@ def parse_metadata(data: ArrayV2Metadata) -> ArrayV2Metadata: def parse_structured_fill_value(fill_value: Any, dtype: np.dtype[Any]) -> Any: """Handle structured dtype/fill value pairs""" try: - if isinstance(fill_value, (tuple, list)): + if isinstance(fill_value, list): + fill_value = tuple(fill_value) + if isinstance(fill_value, tuple): fill_value = np.array([fill_value], dtype=dtype)[0] elif isinstance(fill_value, bytes): fill_value = np.frombuffer(fill_value, dtype=dtype)[0] diff --git a/src/zarr/testing/strategies.py b/src/zarr/testing/strategies.py index 96d664f5aa..358a8736f6 100644 --- a/src/zarr/testing/strategies.py +++ b/src/zarr/testing/strategies.py @@ -5,7 +5,7 @@ import hypothesis.extra.numpy as npst import hypothesis.strategies as st import numpy as np -from hypothesis import event, given, settings # noqa: F401 +from hypothesis import event from hypothesis.strategies import SearchStrategy import zarr diff --git a/tests/test_properties.py b/tests/test_properties.py index 7856770dc6..73b9207590 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -1,6 +1,7 @@ import dataclasses import json import numbers + import numpy as np import pytest from numpy.testing import assert_array_equal diff --git a/tests/test_v2.py b/tests/test_v2.py index 0a4487cfcc..3a1acd064f 100644 --- a/tests/test_v2.py +++ b/tests/test_v2.py @@ -15,6 +15,7 @@ from zarr import config from zarr.abc.store import Store from zarr.core.buffer.core import default_buffer_prototype +from zarr.core.metadata.v2 import parse_structured_fill_value from zarr.core.sync import sync from zarr.storage import MemoryStore, StorePath @@ -315,6 +316,89 @@ def test_structured_dtype_roundtrip(fill_value, tmp_path) -> None: assert (a == za[:]).all() +@pytest.mark.parametrize( + ( + "fill_value", + "dtype", + "expected_result", + ), + [ + ( + ("Alice", 30), + np.dtype([("name", "U10"), ("age", "i4")]), + np.array([("Alice", 30)], dtype=[("name", "U10"), ("age", "i4")])[0], + ), + ( + ["Bob", 25], + np.dtype([("name", "U10"), ("age", "i4")]), + np.array([("Bob", 25)], dtype=[("name", "U10"), ("age", "i4")])[0], + ), + ( + b"\x01\x00\x00\x00\x02\x00\x00\x00", + np.dtype([("x", "i4"), ("y", "i4")]), + np.array([(1, 2)], dtype=[("x", "i4"), ("y", "i4")])[0], + ), + ( + "BQAAAA==", + np.dtype([("val", "i4")]), + np.array([(5,)], dtype=[("val", "i4")])[0], + ), + ( + {"x": 1, "y": 2}, + np.dtype([("location", "O")]), + np.array([({"x": 1, "y": 2},)], dtype=[("location", "O")])[0], + ), + ( + {"x": 1, "y": 2, "z": 3}, + np.dtype([("location", "O")]), + np.array([({"x": 1, "y": 2, "z": 3},)], dtype=[("location", "O")])[0], + ), + ], + ids=[ + "tuple_input", + "list_input", + "bytes_input", + "string_input", + "dictionary_input", + "dictionary_input_extra_fields", + ], +) +def test_parse_structured_fill_value_valid( + fill_value: Any, dtype: np.dtype[Any], expected_result: Any +) -> None: + result = parse_structured_fill_value(fill_value, dtype) + assert result.dtype == expected_result.dtype + assert result == expected_result + if isinstance(expected_result, np.void): + for name in expected_result.dtype.names or []: + assert result[name] == expected_result[name] + + +@pytest.mark.parametrize( + ( + "fill_value", + "dtype", + ), + [ + (("Alice", 30), np.dtype([("name", "U10"), ("age", "i4"), ("city", "U20")])), + (b"\x01\x00\x00\x00", np.dtype([("x", "i4"), ("y", "i4")])), + ("this_is_not_base64", np.dtype([("val", "i4")])), + ("hello", np.dtype([("age", "i4")])), + ({"x": 1, "y": 2}, np.dtype([("location", "i4")])), + ], + ids=[ + "tuple_list_wrong_length", + "bytes_wrong_length", + "invalid_base64", + "wrong_data_type", + "wrong_dictionary", + ], +) +def test_parse_structured_fill_value_invalid(fill_value: Any, dtype: np.dtype[Any]) -> None: + with pytest.raises(ValueError): + parse_structured_fill_value(fill_value, dtype) + + @pytest.mark.parametrize("fill_value", [None, b"x"], ids=["no_fill", "fill"]) def test_other_dtype_roundtrip(fill_value, tmp_path) -> None: a = np.array([b"a\0\0", b"bb", b"ccc"], dtype="V7")