"""`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,)