Skip to content

Commit 702f7b3

Browse files
authored
convert inf, -inf, nan to JSON (#3280)
* convert numpy inf, -inf, nan to JSON * changelog
1 parent a27d4d6 commit 702f7b3

File tree

7 files changed

+44
-24
lines changed

7 files changed

+44
-24
lines changed

changes/3280.fix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a regression introduced in 3.1.0 that prevented ``inf``, ``-inf``, and ``nan`` values
2+
from being stored in ``attributes``.

src/zarr/core/group.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
336336
if self.zarr_format == 3:
337337
return {
338338
ZARR_JSON: prototype.buffer.from_bytes(
339-
json.dumps(self.to_dict(), indent=json_indent, allow_nan=False).encode()
339+
json.dumps(self.to_dict(), indent=json_indent, allow_nan=True).encode()
340340
)
341341
}
342342
else:
@@ -345,7 +345,7 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
345345
json.dumps({"zarr_format": self.zarr_format}, indent=json_indent).encode()
346346
),
347347
ZATTRS_JSON: prototype.buffer.from_bytes(
348-
json.dumps(self.attributes, indent=json_indent, allow_nan=False).encode()
348+
json.dumps(self.attributes, indent=json_indent, allow_nan=True).encode()
349349
),
350350
}
351351
if self.consolidated_metadata:
@@ -373,7 +373,7 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
373373

374374
items[ZMETADATA_V2_JSON] = prototype.buffer.from_bytes(
375375
json.dumps(
376-
{"metadata": d, "zarr_consolidated_format": 1}, allow_nan=False
376+
{"metadata": d, "zarr_consolidated_format": 1}, allow_nan=True
377377
).encode()
378378
)
379379

src/zarr/core/metadata/v2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
132132
json_indent = config.get("json_indent")
133133
return {
134134
ZARRAY_JSON: prototype.buffer.from_bytes(
135-
json.dumps(zarray_dict, indent=json_indent, allow_nan=False).encode()
135+
json.dumps(zarray_dict, indent=json_indent, allow_nan=True).encode()
136136
),
137137
ZATTRS_JSON: prototype.buffer.from_bytes(
138-
json.dumps(zattrs_dict, indent=json_indent, allow_nan=False).encode()
138+
json.dumps(zattrs_dict, indent=json_indent, allow_nan=True).encode()
139139
),
140140
}
141141

src/zarr/core/metadata/v3.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def to_buffer_dict(self, prototype: BufferPrototype) -> dict[str, Buffer]:
288288
d = self.to_dict()
289289
return {
290290
ZARR_JSON: prototype.buffer.from_bytes(
291-
json.dumps(d, allow_nan=False, indent=json_indent).encode()
291+
json.dumps(d, allow_nan=True, indent=json_indent).encode()
292292
)
293293
}
294294

tests/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import annotations
22

3+
import math
34
import os
45
import pathlib
6+
from collections.abc import Mapping, Sequence
57
from dataclasses import dataclass, field
68
from typing import TYPE_CHECKING
79

@@ -442,3 +444,21 @@ def skip_object_dtype(dtype: ZDType[Any, Any]) -> None:
442444
"type resolution"
443445
)
444446
pytest.skip(msg)
447+
448+
449+
def nan_equal(a: object, b: object) -> bool:
450+
"""
451+
Convenience function for equality comparison between two values ``a`` and ``b``, that might both
452+
be NaN. Returns True if both ``a`` and ``b`` are NaN, otherwise returns a == b
453+
"""
454+
if math.isnan(a) and math.isnan(b): # type: ignore[arg-type]
455+
return True
456+
return a == b
457+
458+
459+
def deep_nan_equal(a: object, b: object) -> bool:
460+
if isinstance(a, Mapping) and isinstance(b, Mapping):
461+
return all(deep_nan_equal(a[k], b[k]) for k in a)
462+
if isinstance(a, Sequence) and isinstance(b, Sequence):
463+
return all(deep_nan_equal(a[i], b[i]) for i in range(len(a)))
464+
return nan_equal(a, b)

tests/test_attributes.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1+
import json
2+
from typing import Any
3+
4+
import numpy as np
15
import pytest
26

37
import zarr.core
48
import zarr.core.attributes
59
import zarr.storage
10+
from tests.conftest import deep_nan_equal
11+
from zarr.core.common import ZarrFormat
612

713

8-
def test_put() -> None:
14+
@pytest.mark.parametrize("zarr_format", [2, 3])
15+
@pytest.mark.parametrize(
16+
"data", [{"inf": np.inf, "-inf": -np.inf, "nan": np.nan}, {"a": 3, "c": 4}]
17+
)
18+
def test_put(data: dict[str, Any], zarr_format: ZarrFormat) -> None:
919
store = zarr.storage.MemoryStore()
10-
attrs = zarr.core.attributes.Attributes(
11-
zarr.Group.from_store(store, attributes={"a": 1, "b": 2})
12-
)
13-
attrs.put({"a": 3, "c": 4})
14-
expected = {"a": 3, "c": 4}
15-
assert dict(attrs) == expected
20+
attrs = zarr.core.attributes.Attributes(zarr.Group.from_store(store, zarr_format=zarr_format))
21+
attrs.put(data)
22+
expected = json.loads(json.dumps(data, allow_nan=True))
23+
assert deep_nan_equal(dict(attrs), expected)
1624

1725

1826
def test_asdict() -> None:

tests/test_dtype/test_npy/test_common.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
from __future__ import annotations
22

33
import base64
4-
import math
54
import re
65
import sys
76
from typing import TYPE_CHECKING, Any, get_args
87

98
import numpy as np
109
import pytest
1110

11+
from tests.conftest import nan_equal
1212
from zarr.core.dtype.common import ENDIANNESS_STR, JSONFloatV2, SpecialFloatStrings
1313
from zarr.core.dtype.npy.common import (
1414
NumpyEndiannessStr,
@@ -35,16 +35,6 @@
3535
from zarr.core.common import JSON, ZarrFormat
3636

3737

38-
def nan_equal(a: object, b: object) -> bool:
39-
"""
40-
Convenience function for equality comparison between two values ``a`` and ``b``, that might both
41-
be NaN. Returns True if both ``a`` and ``b`` are NaN, otherwise returns a == b
42-
"""
43-
if math.isnan(a) and math.isnan(b): # type: ignore[arg-type]
44-
return True
45-
return a == b
46-
47-
4838
json_float_v2_roundtrip_cases: tuple[tuple[JSONFloatV2, float | np.floating[Any]], ...] = (
4939
("Infinity", float("inf")),
5040
("Infinity", np.inf),

0 commit comments

Comments
 (0)