- Datatype::parse returns Datatype::Complex for class 11 (also inside
compounds, arrays and VL types) instead of the {r, i} compound view.
- Facade: DType::Complex(Box<DType>); read_complex_f32/f64 accept it.
- h5rs dump/ls/diff print native complex as h5dump/h5ls/h5diff 2.2.0 do
(checked against a fixture written by h5py 3.16 / libhdf5 2.0.0);
dump --json keeps the {r, i} compound (hdf5-json has no complex class).
- clawhdf5-wasm reads native complex datasets as [re, im] pairs.
- Python: clawhdf5.File(path, 'w', libver=...) with h5py's values,
mapped to FileBuilder::libver_bounds; 'v108' output opens in HDF5 1.8.23.
- Docs: known-issues entry moved to Fixed (history), CHANGELOG, READMEs.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
144 lines
4.8 KiB
Python
144 lines
4.8 KiB
Python
"""`clawhdf5.File(path, 'w', libver=...)`: h5py's library version bounds.
|
|
|
|
A file written with libver='v108' must open in HDF5 1.8. Its h5dump is
|
|
found through CLAWHDF5_H5DUMP18 or at ~/.cache/hdf5-1.8.23/bin/h5dump
|
|
(scripts/build-hdf5-1.8.sh builds it); without it that check is skipped.
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
import clawhdf5
|
|
|
|
|
|
def superblock_version(path):
|
|
with open(path, "rb") as f:
|
|
head = f.read(9)
|
|
assert head[:8] == b"\x89HDF\r\n\x1a\n"
|
|
return head[8]
|
|
|
|
|
|
def h5dump18():
|
|
path = os.environ.get("CLAWHDF5_H5DUMP18") or os.path.expanduser(
|
|
"~/.cache/hdf5-1.8.23/bin/h5dump"
|
|
)
|
|
try:
|
|
out = subprocess.run([path, "--version"], capture_output=True, text=True)
|
|
except OSError:
|
|
return None
|
|
return path if "1.8." in out.stdout else None
|
|
|
|
|
|
def write_sample(path, libver):
|
|
with clawhdf5.File(path, "w", libver=libver) as f:
|
|
f.create_dataset("x", data=np.arange(10, dtype=np.float64))
|
|
f.create_dataset(
|
|
"chunked",
|
|
data=np.arange(100, dtype=np.int32),
|
|
chunks=(30,),
|
|
compression="gzip",
|
|
)
|
|
f.create_dataset("z", data=np.array([1 + 2j, -3j], dtype=np.complex128))
|
|
g = f.create_group("g")
|
|
g.create_dataset("y", data=np.ones(3, dtype=np.float32))
|
|
f.attrs["version"] = 1
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"libver, sb",
|
|
[
|
|
(None, 3),
|
|
("v108", 2),
|
|
(("v108", "v108"), 2),
|
|
(("v108", "latest"), 2),
|
|
("v110", 3),
|
|
("v112", 3),
|
|
("v114", 3),
|
|
("v200", 3),
|
|
("latest", 3),
|
|
(("v110", "v200"), 3),
|
|
],
|
|
)
|
|
def test_libver_bounds(h5py, tmp_path, libver, sb):
|
|
path = str(tmp_path / "libver.h5")
|
|
write_sample(path, libver)
|
|
# v108 low bound: HDF5 1.8's version-2 superblock; 1.10 and later: 3.
|
|
assert superblock_version(path) == sb
|
|
with h5py.File(path, "r") as f:
|
|
np.testing.assert_array_equal(f["x"][:], np.arange(10.0))
|
|
np.testing.assert_array_equal(f["chunked"][:], np.arange(100))
|
|
np.testing.assert_array_equal(f["z"][:], [1 + 2j, -3j])
|
|
np.testing.assert_array_equal(f["g/y"][:], np.ones(3))
|
|
assert f.attrs["version"] == 1
|
|
with clawhdf5.File(path, "r") as f:
|
|
np.testing.assert_array_equal(f["chunked"][:], np.arange(100))
|
|
|
|
|
|
def test_libver_v108_opens_in_hdf5_1_8(tmp_path):
|
|
h5dump = h5dump18()
|
|
if h5dump is None:
|
|
pytest.skip("no HDF5 1.8 h5dump (CLAWHDF5_H5DUMP18)")
|
|
path = str(tmp_path / "v108.h5")
|
|
write_sample(path, "v108")
|
|
out = subprocess.run([h5dump, path], capture_output=True, text=True)
|
|
assert out.returncode == 0, out.stderr
|
|
assert "h5dump error" not in out.stderr
|
|
assert 'DATASET "y"' in out.stdout
|
|
# The chunked, deflated dataset (a version-1 B-tree) reads in full.
|
|
out = subprocess.run(
|
|
[h5dump, "-w", "0", "-d", "/chunked", path], capture_output=True, text=True
|
|
)
|
|
assert out.returncode == 0, out.stderr
|
|
assert "(0): " + ", ".join(str(k) for k in range(100)) + "\n" in out.stdout
|
|
# The default (1.10 format) does not open in 1.8: the bound matters.
|
|
path = str(tmp_path / "default.h5")
|
|
write_sample(path, None)
|
|
out = subprocess.run([h5dump, path], capture_output=True, text=True)
|
|
assert out.returncode != 0
|
|
|
|
|
|
def test_libver_earliest_writes_v108_with_a_warning(h5py, tmp_path):
|
|
path = str(tmp_path / "earliest.h5")
|
|
with pytest.warns(UserWarning, match="pre-1.8"):
|
|
write_sample(path, "earliest")
|
|
assert superblock_version(path) == 2
|
|
with h5py.File(path, "r") as f:
|
|
np.testing.assert_array_equal(f["x"][:], np.arange(10.0))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"libver, match",
|
|
[
|
|
(("v108", "earliest"), "high bound 'earliest'"),
|
|
("v109", "unknown libver 'v109'"),
|
|
(("latest", "v108"), "newer than the high bound"),
|
|
(("v108",), "tuple"),
|
|
(3, "tuple"),
|
|
],
|
|
)
|
|
def test_libver_errors(tmp_path, libver, match):
|
|
with pytest.raises(ValueError, match=match):
|
|
clawhdf5.File(str(tmp_path / "bad.h5"), "w", libver=libver)
|
|
|
|
|
|
def test_libver_v108_high_bound_writes_everything(tmp_path):
|
|
# Native complex numbers need HDF5 2.0; the Python writer stores complex
|
|
# as h5py's {r, i} compound, which 1.8 reads, so the 1.8 high bound is
|
|
# fine for everything clawhdf5.File writes.
|
|
path = str(tmp_path / "v18only.h5")
|
|
write_sample(path, ("v108", "v108"))
|
|
assert superblock_version(path) == 2
|
|
|
|
|
|
def test_libver_not_for_editing(tmp_path):
|
|
path = str(tmp_path / "e.h5")
|
|
write_sample(path, None)
|
|
with pytest.raises(NotImplementedError, match="libver"):
|
|
clawhdf5.File(path, "r+", libver="latest")
|
|
# Reading ignores it, as the bounds only affect what is written.
|
|
with clawhdf5.File(path, "r", libver="v108") as f:
|
|
assert f["x"].shape == (10,)
|