Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/3478.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Attributes holding numpy scalars, such as `np.float32(1.5)`, `np.int64(3)` or `np.bool_(True)`, can be written again. They were previously rejected with `Object of type float32 is not JSON serializable`.
15 changes: 13 additions & 2 deletions src/zarr/core/_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
from __future__ import annotations

import json
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, Any, cast

import numpy as np

from zarr.core.buffer import default_buffer_prototype

Expand All @@ -35,6 +37,13 @@
from zarr.core.common import JSON


def _json_default(obj: Any) -> Any:
"""Convert a numpy scalar, which `json` cannot encode, to the equivalent Python value."""
if isinstance(obj, np.generic):
return obj.item()
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")


def buffer_to_json(buffer: Buffer) -> JSON:
"""Parse the contents of a `Buffer` as a JSON value."""
# json.loads is typed as returning Any; the result is by definition JSON.
Expand Down Expand Up @@ -96,7 +105,9 @@ def json_to_buffer(
"""
if prototype is None:
prototype = default_buffer_prototype()
return prototype.buffer.from_bytes(json.dumps(obj, indent=indent, allow_nan=allow_nan).encode())
return prototype.buffer.from_bytes(
json.dumps(obj, indent=indent, allow_nan=allow_nan, default=_json_default).encode()
)


async def get_json(store: Store, key: str, *, byte_range: ByteRequest | None = None) -> JSON | None:
Expand Down
14 changes: 14 additions & 0 deletions tests/test_attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ def test_put(data: dict[str, Any], zarr_format: ZarrFormat) -> None:
assert deep_nan_equal(dict(attrs), expected)


@pytest.mark.parametrize("zarr_format", [2, 3])
@pytest.mark.parametrize(
"value",
[np.float32(1.5), np.float32(np.nan), np.int64(3), np.uint8(7), np.bool_(True)],
)
def test_numpy_scalar_attribute_roundtrip(value: Any, zarr_format: ZarrFormat) -> None:
"""numpy scalars are stored as their Python equivalents and survive reopening the store."""
store = zarr.storage.MemoryStore()
zarr.create_group(store, zarr_format=zarr_format, attributes={"foo": value})
reopened = zarr.open_group(store, mode="r", zarr_format=zarr_format)
expected = value.item()
assert deep_nan_equal({"foo": reopened.attrs["foo"]}, {"foo": expected})


def test_asdict() -> None:
store = zarr.storage.MemoryStore()
attrs = zarr.core.attributes.Attributes(
Expand Down
Loading