Skip to content

Commit 9048d79

Browse files
committed
Fix fill_value serialization of NaN; add property-based tests
1 parent a52048d commit 9048d79

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

src/zarr/core/metadata/v2.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,26 @@ def _json_convert(
144144
return o.name
145145
raise TypeError
146146

147+
def _sanitize_fill_value(fv: Any):
148+
if isinstance(fv, (float, np.floating)):
149+
if np.isnan(fv):
150+
fv = "NaN"
151+
elif np.isinf(fv):
152+
fv = "Infinity" if fv > 0 else "-Infinity"
153+
elif isinstance(fv, (np.complex64, np.complexfloating)):
154+
fv = [_sanitize_fill_value(fv.real), _sanitize_fill_value(fv.imag)]
155+
return fv
156+
147157
zarray_dict = self.to_dict()
158+
zarray_dict["fill_value"] = _sanitize_fill_value(zarray_dict["fill_value"])
148159
zattrs_dict = zarray_dict.pop("attributes", {})
149160
json_indent = config.get("json_indent")
150161
return {
151162
ZARRAY_JSON: prototype.buffer.from_bytes(
152-
json.dumps(zarray_dict, default=_json_convert, indent=json_indent).encode()
163+
json.dumps(zarray_dict, default=_json_convert, indent=json_indent, allow_nan=False).encode()
153164
),
154165
ZATTRS_JSON: prototype.buffer.from_bytes(
155-
json.dumps(zattrs_dict, indent=json_indent).encode()
166+
json.dumps(zattrs_dict, indent=json_indent, allow_nan=False).encode()
156167
),
157168
}
158169

@@ -300,7 +311,6 @@ def parse_fill_value(fill_value: object, dtype: np.dtype[Any]) -> Any:
300311
-------
301312
An instance of `dtype`, or `None`, or any python object (in the case of an object dtype)
302313
"""
303-
304314
if fill_value is None or dtype.hasobject:
305315
# no fill value
306316
pass

src/zarr/testing/v2metadata.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from hypothesis import strategies as st
2+
import hypothesis.extra.numpy as npst
3+
4+
from zarr.testing.strategies import v2_dtypes
5+
6+
@st.composite
7+
def array_metadata_v2_inputs(draw):
8+
dims = draw(st.integers(min_value=1, max_value=10))
9+
shape = draw(st.lists(st.integers(min_value=1, max_value=100), min_size=dims, max_size=dims))
10+
chunks = draw(st.lists(st.integers(min_value=1, max_value=100), min_size=dims, max_size=dims))
11+
dtype = draw(v2_dtypes())
12+
fill_value = draw(st.one_of([st.none(), npst.from_dtype(dtype)]))
13+
order = draw(st.sampled_from(["C", "F"]))
14+
dimension_separator = draw(st.sampled_from([".", "/"]))
15+
return {
16+
"shape": shape,
17+
"dtype": dtype,
18+
"chunks": chunks,
19+
"fill_value": fill_value,
20+
"order": order,
21+
"dimension_separator": dimension_separator,
22+
}

tests/test_properties.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import numpy as np
23
import pytest
34
from numpy.testing import assert_array_equal
@@ -8,7 +9,11 @@
89
import hypothesis.strategies as st
910
from hypothesis import given
1011

12+
from zarr.core.buffer import default_buffer_prototype
13+
from zarr.core.common import ZARRAY_JSON, parse_shapelike
14+
from zarr.core.metadata.v2 import ArrayV2Metadata, parse_fill_value, parse_dtype, parse_shapelike
1115
from zarr.testing.strategies import arrays, basic_indices, numpy_arrays, zarr_formats
16+
from zarr.testing.v2metadata import array_metadata_v2_inputs
1217

1318

1419
@given(data=st.data(), zarr_format=zarr_formats)
@@ -69,3 +74,38 @@ def test_vindex(data: st.DataObject) -> None:
6974
# nparray = data.draw(np_arrays)
7075
# zarray = data.draw(arrays(arrays=st.just(nparray)))
7176
# assert_array_equal(nparray, zarray[:])
77+
78+
79+
@given(array_metadata_v2_inputs())
80+
def test_v2meta_fill_value_serialization(inputs):
81+
metadata = ArrayV2Metadata(**inputs)
82+
buffer_dict = metadata.to_buffer_dict(prototype=default_buffer_prototype())
83+
zarray_dict = json.loads(buffer_dict[ZARRAY_JSON].to_bytes().decode())
84+
85+
if isinstance(inputs["fill_value"], (float, np.floating)) and np.isnan(inputs["fill_value"]):
86+
assert zarray_dict["fill_value"] == "NaN"
87+
else:
88+
assert zarray_dict["fill_value"] == inputs["fill_value"]
89+
90+
91+
@given(npst.from_dtype(dtype=np.dtype("float64"), allow_nan=True, allow_infinity=True))
92+
def test_v2meta_nan_and_infinity(fill_value):
93+
metadata = ArrayV2Metadata(
94+
shape=[10],
95+
dtype=np.dtype("float64"),
96+
chunks=[5],
97+
fill_value=fill_value,
98+
order="C",
99+
)
100+
101+
buffer_dict = metadata.to_buffer_dict(prototype=default_buffer_prototype())
102+
zarray_dict = json.loads(buffer_dict[ZARRAY_JSON].to_bytes().decode())
103+
104+
if np.isnan(fill_value):
105+
assert zarray_dict["fill_value"] == "NaN"
106+
elif np.isinf(fill_value) and fill_value > 0:
107+
assert zarray_dict["fill_value"] == "Infinity"
108+
elif np.isinf(fill_value):
109+
assert zarray_dict["fill_value"] == "-Infinity"
110+
else:
111+
assert zarray_dict["fill_value"] == fill_value

0 commit comments

Comments
 (0)