"""Every read through clawhdf5 compared against h5py (libhdf5) on files h5py writes: dtypes, shapes, values and the type of what comes back (array, numpy scalar, bytes, str, Empty), for every datatype the bindings map and a spread of index expressions; plus the errors h5py gives for the same keys.""" import threading from concurrent.futures import ThreadPoolExecutor import numpy as np import pytest import clawhdf5 # --------------------------------------------------------------------------- # The generated file # --------------------------------------------------------------------------- NUMERIC = [ "i2", ">i4", ">i8", ">u2", ">u4", ">u8", "f2", ">f4", ">f8", ] def _values(dtype, shape, seed): rng = np.random.default_rng(seed) dt = np.dtype(dtype) n = int(np.prod(shape)) if dt.kind == "f": return rng.standard_normal(n).astype(dt).reshape(shape) info = np.iinfo(dt) return rng.integers(info.min, info.max, size=n, dtype=dt.newbyteorder("=")).astype(dt).reshape(shape) def _compound_dtype(): return np.dtype( { "names": ["id", "pos", "label", "flag", "vec"], "formats": ["f8", "S6", "u1", ("", "be_") f.create_dataset(f"num/{name}_1d", data=_values(dt, (37,), i)) f.create_dataset( f"num/{name}_2d_gzip", data=_values(dt, (13, 11), 100 + i), chunks=(4, 5), compression="gzip", ) f.create_dataset("num/f8_3d", data=_values("f8")) fseqs = np.empty(3, dtype=object) fseqs[:] = [np.linspace(0, 1, 5).astype(">f8"), np.array([2.5], dtype=">f8"), np.array([], dtype=">f8")] f.create_dataset("vlen/f8_be", data=fseqs, dtype=vl_f) # Compounds. cdt = _compound_dtype() rec = np.zeros(10, dtype=cdt) rec["id"] = np.arange(10) * 3 rec["pos"] = np.linspace(-1, 1, 10) rec["label"] = [f"n{i}".encode() for i in range(10)] rec["flag"] = np.arange(10) % 2 rec["vec"] = np.arange(30, dtype=" 1 else [0], (Ellipsis,)] if n0 > 5: keys += [[1, 2, 3, 5], [0, 4, 5]] if len(shape) >= 2: n1 = shape[1] keys += [ (0, 0), (-1, -1), (slice(1, 3), slice(None, None, 2)), (Ellipsis, 1), (1, Ellipsis), (slice(None), [0, n1 - 1] if n1 > 1 else [0]), (slice(None, None, 2), 1), (slice(0, 2), slice(3, 1)), (np.array(1) if n0 > 1 else np.array(0), slice(None)), (slice(None), np.array(n1 - 1, dtype="u2")), ] if len(shape) >= 3: keys += [(0, slice(None), -1), (slice(1, None, 2), 2, slice(None, None, 3)), (Ellipsis, 0, 0), (0, Ellipsis, 1)] return keys # h5py 3.16 (HDF5 2.0) returns the elements of a variable-length sequence of # big-endian floats unswapped (0.25 comes back as 2.6e-319), so these are # checked against the values written instead (test_vlen_big_endian). H5PY_MISREADS = {"vlen/f8_be"} ERROR_KEYS_1D = [slice(None, None, -1), 10**6, -(10**6), None, (0, 0, 0, 0, 0), [3, 1], (Ellipsis, Ellipsis), 1.5, "nope"] # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- def test_every_dataset_matches_h5py(h5py, pair): ours, theirs, _ = pair checked = 0 for name in _all_datasets(h5py, theirs): if name.startswith("unsupported/") or name in H5PY_MISREADS or name == "cmp/with_vlen": continue t = theirs[name] o = ours[name] assert o.shape == t.shape, name assert o.dtype == t.dtype, f"{name}: {o.dtype} vs {t.dtype}" assert dict(o.dtype.metadata or {}) == dict(t.dtype.metadata or {}), name assert o.ndim == t.ndim and o.size == t.size, name assert o.maxshape == t.maxshape, name assert o.name == t.name, name if t.shape is None: assert_same(o[()], t[()], name) continue for key in keys_for(t.shape): what = f"{name}[{key!r}]" try: expected = t[key] except Exception as e: # noqa: BLE001 - h5py refuses: so must we with pytest.raises(type(e)): o[key] continue assert_same(o[key], expected, what) checked += 1 assert checked > 500 def test_vlen_big_endian(pair): ours, _, _ = pair ds = ours["vlen/f8_be"] assert ds.dtype.metadata["vlen"] == np.dtype(">f8") got = ds[()] expected = [np.linspace(0, 1, 5), np.array([2.5]), np.array([])] assert got.shape == (3,) for g, e in zip(got, expected): assert g.dtype == np.dtype(">f8") np.testing.assert_array_equal(g, e) np.testing.assert_array_equal(ds[1], [2.5]) def test_errors_match_h5py(h5py, pair): ours, theirs, _ = pair for name in ["num/le_i4_1d", "num/be_f8_2d_gzip", "str/vlen", "num/scalar_f8"]: for key in ERROR_KEYS_1D: try: theirs[name][key] except Exception as e: # noqa: BLE001 with pytest.raises(type(e)): ours[name][key] else: pass # valid for this shape; covered by the value test def test_compound_fields_match_h5py(pair): ours, theirs, _ = pair for name in ["cmp/padded", "cmp/padded_chunked", "cmp/nested_2d"]: t, o = theirs[name], ours[name] for field in t.dtype.names: assert_same(o[field], t[field], f"{name}[{field}]") assert_same(o[field, 1:3], t[field, 1:3], f"{name}[{field}, 1:3]") two = list(t.dtype.names[:2]) expected = t[tuple(two)] got = o[tuple(two)] assert got.dtype.names == expected.dtype.names for field in two: np.testing.assert_array_equal(got[field], expected[field]) with pytest.raises(ValueError): ours["cmp/padded"]["no_such_field"] with pytest.raises(ValueError): ours["num/le_i4_1d"]["id"] def test_numpy_asarray_and_len(pair): ours, theirs, _ = pair assert_same(np.asarray(ours["num/f8_3d"]), np.asarray(theirs["num/f8_3d"])) assert len(ours["num/f8_3d"]) == len(theirs["num/f8_3d"]) with pytest.raises(TypeError): len(ours["num/scalar_f8"]) def test_attributes_match_h5py(h5py, pair): ours, theirs, _ = pair for path in ["/", "num/le_f8_1d", "deep/er/est"]: t = theirs[path].attrs o = ours[path].attrs assert list(o.keys()) == sorted(t.keys()), path assert len(o) == len(t) for k in t.keys(): assert k in o assert_same(o[k], t[k], f"{path}.attrs[{k}]") assert o.get("missing", 5) == 5 with pytest.raises(KeyError): o["missing"] assert [k for k, _ in o.items()] == list(o.keys()) def test_groups_match_h5py(h5py, pair): ours, theirs, _ = pair assert list(ours.keys()) == list(theirs.keys()) assert len(ours) == len(theirs) for path in ["num", "deep", "deep/er", "deep/er/est", "empty_group"]: o, t = ours[path], theirs[path] assert isinstance(o, clawhdf5.Group) assert list(o.keys()) == list(t.keys()), path assert list(o) == list(t), path assert len(o) == len(t), path assert o.name == t.name g = ours["deep/er"] assert isinstance(g["est"], clawhdf5.Group) assert isinstance(g["est/leaf"], clawhdf5.Dataset) assert g["/deep/er/est/leaf"].name == "/deep/er/est/leaf" assert "est/leaf" in g and "/num" in g and "nope" not in g assert ours["/"].name == "/" assert "deep/er/est/leaf" in ours assert ours.get("nope") is None with pytest.raises(KeyError): ours["deep/nope"] names = [k for k, v in ours["deep/er/est"].items()] assert names == ["leaf"] assert isinstance(ours["deep/er/est"].values()[0], clawhdf5.Dataset) def test_unsupported_types_are_errors_not_data(pair): ours, _, _ = pair ds = ours["unsupported/refs"] # opening works with pytest.raises(TypeError): ds.dtype with pytest.raises(TypeError): ds[()] with pytest.raises(TypeError): ours["cmp/with_vlen"][()] def test_boolean_masks_are_refused(pair): ours, _, _ = pair with pytest.raises(TypeError): ours["num/le_i4_1d"][np.ones(37, dtype=bool)] def test_reads_hand_numpy_the_rust_buffer(pair): """A fixed-size read is a view over the buffer the library filled, not a copy of it.""" ours, _, _ = pair arr = ours["num/le_f8_2d_gzip"][2:9, 1:4] assert not arr.flags.owndata assert arr.base is not None assert arr.flags.aligned and arr.flags.c_contiguous def test_only_the_selected_chunks_are_read(h5py, tmp_path): """Damage one chunk: a selection that avoids it still reads, one that touches it fails. Reading everything and slicing afterwards (what the bindings did before) failed both. (The library reads the whole dataset anyway when a selection's bounding box covers more than half of it, so the selections here stay below that.)""" path = str(tmp_path / "damaged.h5") data = np.arange(1000, dtype="