Skip to content

Commit 5be99ea

Browse files
committed
fix: stringify HDF5 reference attributes in the h5 browser
Fixes #9
1 parent 4fe70f5 commit 5be99ea

3 files changed

Lines changed: 55 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
1515

1616
- **Saving and opening TIFF images**: saving (or opening) an image in TIFF format no longer fails with an internal `No module named 'tifffile'` error. The library required for TIFF support is now installed alongside the rest of the scientific stack at startup, so `.tif`/`.tiff` files can be written and read like any other image format.
1717
- **Command palette search**: short queries no longer flood the results with unrelated commands. Typing `rota` (looking for _Rotation_) used to also match entries like _Import annotations_, _Polynomial calibration_ or _Horizontal projection_, because the search accepted any command whose letters appeared in order, however scattered. The search now keeps only commands that either contain the query as-is or match it word-by-word (initials), so results stay relevant while still finding commands like `fft` or `fan` → _Fourier analysis_.
18+
- **HDF5 files converted from HDF4**: opening an HDF5 file produced by `h4toh5convert` no longer breaks the Properties panel (with a `could not be cloned` error) nor the display of imported signals and images. Such files carry HDF5 object-reference attributes (e.g. `DIMENSION_LIST`) that could not be transferred out of the computation worker; these attributes are now shown as plain text instead, so the data imports and displays normally.
1819

1920
## [0.6.2] - 2026-06-26
2021

‎src/runtime/dlw_h5browser.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,9 +431,19 @@ def _collect_attrs(dset: Any) -> dict[str, Any]:
431431
if isinstance(value, bytes):
432432
value = _safe_decode_bytes(value)
433433
if isinstance(value, np.ndarray):
434-
try:
435-
value = value.tolist()
436-
except Exception: # noqa: BLE001
434+
# Numeric, boolean and string arrays convert cleanly to
435+
# JSON-friendly lists. ``object``/``void`` dtypes are how h5py
436+
# exposes HDF5 object/region *reference* attributes (e.g.
437+
# ``DIMENSION_LIST`` / ``REFERENCE_LIST`` produced by
438+
# ``h4toh5convert``): ``tolist()`` would yield ``h5py.h5r.Reference``
439+
# elements that cannot cross the worker→main-thread ``postMessage``
440+
# boundary ("could not be cloned"), so stringify them instead.
441+
if value.dtype.kind in "biufcSU":
442+
try:
443+
value = value.tolist()
444+
except Exception: # noqa: BLE001
445+
value = repr(value)
446+
else:
437447
value = repr(value)
438448
elif isinstance(value, (np.integer,)):
439449
value = int(value)

‎tests/python/test_h5browser.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,47 @@ def test_node_attrs_exposes_attributes(opened):
9191
assert attrs["attributes"].get("units") == "V"
9292

9393

94+
def _assert_no_h5_references(value):
95+
"""Recursively assert *value* holds no ``h5py.h5r.Reference`` object."""
96+
assert not isinstance(value, h5py.h5r.Reference)
97+
if isinstance(value, dict):
98+
for item in value.values():
99+
_assert_no_h5_references(item)
100+
elif isinstance(value, (list, tuple)):
101+
for item in value:
102+
_assert_no_h5_references(item)
103+
104+
105+
def test_node_attrs_stringifies_reference_attributes(tmp_path):
106+
"""HDF5 reference attributes must not leak non-cloneable objects.
107+
108+
Files converted with ``h4toh5convert`` carry object-reference attributes
109+
(e.g. ``DIMENSION_LIST``) that h5py exposes as ``object``-dtype arrays of
110+
:class:`h5py.h5r.Reference`. Such objects cannot cross the worker→main
111+
thread ``postMessage`` boundary ("could not be cloned"), so
112+
``_collect_attrs`` must stringify them rather than ``tolist()`` them.
113+
"""
114+
path = tmp_path / "ref.h5"
115+
ref_dtype = h5py.special_dtype(ref=h5py.Reference)
116+
with h5py.File(path, "w") as f:
117+
img = f.create_dataset("img", data=np.arange(64, dtype=float).reshape(8, 8))
118+
s0 = f.create_dataset("scale0", data=np.arange(8))
119+
s1 = f.create_dataset("scale1", data=np.arange(8))
120+
img.attrs.create("DIMENSION_LIST", np.array([s0.ref, s1.ref], dtype=ref_dtype))
121+
img.attrs["REFERENCE"] = s0.ref
122+
img.attrs["units"] = "px"
123+
result = h5b.open_file("ref.h5", path.read_bytes())
124+
try:
125+
attrs = h5b.node_attrs(result["file_id"], "/img")["attributes"]
126+
assert attrs["units"] == "px"
127+
# Reference attributes survive only as harmless strings.
128+
assert isinstance(attrs["DIMENSION_LIST"], str)
129+
assert isinstance(attrs["REFERENCE"], str)
130+
_assert_no_h5_references(attrs)
131+
finally:
132+
h5b.close_all()
133+
134+
94135
def test_preview_signal(opened):
95136
file_id = opened["file_id"]
96137
prev = h5b.preview(file_id, "/sig")

0 commit comments

Comments
 (0)