Skip to content

Commit

Permalink
Fix fill_value serialization of NaN; add property-based tests
Browse files Browse the repository at this point in the history
  • Loading branch information
moradology committed Feb 5, 2025
1 parent a52048d commit 9048d79
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 3 deletions.
16 changes: 13 additions & 3 deletions src/zarr/core/metadata/v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,15 +144,26 @@ def _json_convert(
return o.name
raise TypeError

def _sanitize_fill_value(fv: Any):
if isinstance(fv, (float, np.floating)):
if np.isnan(fv):
fv = "NaN"
elif np.isinf(fv):
fv = "Infinity" if fv > 0 else "-Infinity"
elif isinstance(fv, (np.complex64, np.complexfloating)):
fv = [_sanitize_fill_value(fv.real), _sanitize_fill_value(fv.imag)]
return fv

Check warning on line 155 in src/zarr/core/metadata/v2.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/core/metadata/v2.py#L147-L155

Added lines #L147 - L155 were not covered by tests

zarray_dict = self.to_dict()
zarray_dict["fill_value"] = _sanitize_fill_value(zarray_dict["fill_value"])

Check warning on line 158 in src/zarr/core/metadata/v2.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/core/metadata/v2.py#L158

Added line #L158 was not covered by tests
zattrs_dict = zarray_dict.pop("attributes", {})
json_indent = config.get("json_indent")
return {
ZARRAY_JSON: prototype.buffer.from_bytes(
json.dumps(zarray_dict, default=_json_convert, indent=json_indent).encode()
json.dumps(zarray_dict, default=_json_convert, indent=json_indent, allow_nan=False).encode()
),
ZATTRS_JSON: prototype.buffer.from_bytes(
json.dumps(zattrs_dict, indent=json_indent).encode()
json.dumps(zattrs_dict, indent=json_indent, allow_nan=False).encode()
),
}

Expand Down Expand Up @@ -300,7 +311,6 @@ def parse_fill_value(fill_value: object, dtype: np.dtype[Any]) -> Any:
-------
An instance of `dtype`, or `None`, or any python object (in the case of an object dtype)
"""

if fill_value is None or dtype.hasobject:
# no fill value
pass
Expand Down
22 changes: 22 additions & 0 deletions src/zarr/testing/v2metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from hypothesis import strategies as st
import hypothesis.extra.numpy as npst

from zarr.testing.strategies import v2_dtypes

@st.composite
def array_metadata_v2_inputs(draw):
dims = draw(st.integers(min_value=1, max_value=10))
shape = draw(st.lists(st.integers(min_value=1, max_value=100), min_size=dims, max_size=dims))
chunks = draw(st.lists(st.integers(min_value=1, max_value=100), min_size=dims, max_size=dims))
dtype = draw(v2_dtypes())
fill_value = draw(st.one_of([st.none(), npst.from_dtype(dtype)]))
order = draw(st.sampled_from(["C", "F"]))
dimension_separator = draw(st.sampled_from([".", "/"]))
return {

Check warning on line 15 in src/zarr/testing/v2metadata.py

View check run for this annotation

Codecov / codecov/patch

src/zarr/testing/v2metadata.py#L8-L15

Added lines #L8 - L15 were not covered by tests
"shape": shape,
"dtype": dtype,
"chunks": chunks,
"fill_value": fill_value,
"order": order,
"dimension_separator": dimension_separator,
}
40 changes: 40 additions & 0 deletions tests/test_properties.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import numpy as np
import pytest
from numpy.testing import assert_array_equal
Expand All @@ -8,7 +9,11 @@
import hypothesis.strategies as st
from hypothesis import given

from zarr.core.buffer import default_buffer_prototype
from zarr.core.common import ZARRAY_JSON, parse_shapelike
from zarr.core.metadata.v2 import ArrayV2Metadata, parse_fill_value, parse_dtype, parse_shapelike
from zarr.testing.strategies import arrays, basic_indices, numpy_arrays, zarr_formats
from zarr.testing.v2metadata import array_metadata_v2_inputs


@given(data=st.data(), zarr_format=zarr_formats)
Expand Down Expand Up @@ -69,3 +74,38 @@ def test_vindex(data: st.DataObject) -> None:
# nparray = data.draw(np_arrays)
# zarray = data.draw(arrays(arrays=st.just(nparray)))
# assert_array_equal(nparray, zarray[:])


@given(array_metadata_v2_inputs())
def test_v2meta_fill_value_serialization(inputs):
metadata = ArrayV2Metadata(**inputs)
buffer_dict = metadata.to_buffer_dict(prototype=default_buffer_prototype())
zarray_dict = json.loads(buffer_dict[ZARRAY_JSON].to_bytes().decode())

if isinstance(inputs["fill_value"], (float, np.floating)) and np.isnan(inputs["fill_value"]):
assert zarray_dict["fill_value"] == "NaN"
else:
assert zarray_dict["fill_value"] == inputs["fill_value"]


@given(npst.from_dtype(dtype=np.dtype("float64"), allow_nan=True, allow_infinity=True))
def test_v2meta_nan_and_infinity(fill_value):
metadata = ArrayV2Metadata(
shape=[10],
dtype=np.dtype("float64"),
chunks=[5],
fill_value=fill_value,
order="C",
)

buffer_dict = metadata.to_buffer_dict(prototype=default_buffer_prototype())
zarray_dict = json.loads(buffer_dict[ZARRAY_JSON].to_bytes().decode())

if np.isnan(fill_value):
assert zarray_dict["fill_value"] == "NaN"
elif np.isinf(fill_value) and fill_value > 0:
assert zarray_dict["fill_value"] == "Infinity"
elif np.isinf(fill_value):
assert zarray_dict["fill_value"] == "-Infinity"
else:
assert zarray_dict["fill_value"] == fill_value

0 comments on commit 9048d79

Please sign in to comment.