Skip to content

HDF5

HDF5 files as a tree of groups and datasets, readable and writable without h5py.

:func:write and :func:read work on :class:Group and :class:Dataset; :func:update_attrs changes the attributes of an existing file. h5py is used when it is installed (the hdf5 extra); without it, files are written by a small built-in writer and read with pyfive, a pure-Python HDF5 reader. Either way the files are ordinary HDF5: h5py, GWpy and the HDF5 tools read what the built-in writer writes, and the built-in path reads what they write.

The built-in writer covers what the gwmock packages store: datasets of booleans, integers, 32- and 64-bit floats, fixed-length strings and compound rows of those; attributes of numbers, arrays and strings, a string of any length (stored, as h5py stores it, as a variable-length string), a number or array up to 64 KiB; nested groups. It writes data uncompressed; files h5py wrote with gzip compression read either way.

Dataset dataclass

An HDF5 dataset: an array and its attributes. A str is stored as a fixed-length UTF-8 string.

data is None for a dataset read with read(..., data=False).

Source code in src/gwmock_data/hdf5/__init__.py
34
35
36
37
38
39
40
41
42
@dataclass
class Dataset:
    """An HDF5 dataset: an array and its attributes. A ``str`` is stored as a fixed-length UTF-8 string.

    ``data`` is ``None`` for a dataset read with ``read(..., data=False)``.
    """

    data: Any
    attrs: dict[str, Any] = field(default_factory=dict)

Group dataclass

An HDF5 group: named members (groups or datasets) and attributes. The file itself is the root group.

Source code in src/gwmock_data/hdf5/__init__.py
45
46
47
48
49
50
@dataclass
class Group:
    """An HDF5 group: named members (groups or datasets) and attributes. The file itself is the root group."""

    members: dict[str, Group | Dataset] = field(default_factory=dict)
    attrs: dict[str, Any] = field(default_factory=dict)

read(path, *, backend='auto', members=None, data=True)

Read the HDF5 file at path into a :class:Group.

Datasets are read into NumPy arrays (a scalar as a 0-d array). A scalar string, as a dataset or an attribute, is returned as str; an array of fixed-length strings stays a NumPy array of bytes.

Parameters:

Name Type Description Default
path str | Path

Input file.

required
backend Backend

"h5py", "builtin" (pyfive), or "auto" for h5py when it is installed and pyfive otherwise.

'auto'
members Iterable[str] | None

Names of the top-level members to read; the others are not read at all, which matters for a large file of which only a few columns are wanted. A name the file lacks is absent from the result. None, the default, reads every member. The file's own attributes are always read.

None
data bool

False reads the structure and the attributes only: every :class:Dataset comes back with data=None and its samples are never read, which matters for inspecting the metadata of a large file.

True

Raises:

Type Description
ModuleNotFoundError

If backend="h5py" and h5py is not installed.

TypeError

If members is a single string rather than a collection of names.

ValueError

If a name in members is empty, . or .., or contains /: only top-level members can be selected, not paths into groups.

Source code in src/gwmock_data/hdf5/__init__.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def read(
    path: str | Path, *, backend: Backend = "auto", members: Iterable[str] | None = None, data: bool = True
) -> Group:
    """Read the HDF5 file at ``path`` into a :class:`Group`.

    Datasets are read into NumPy arrays (a scalar as a 0-d array). A scalar string, as a dataset or an
    attribute, is returned as ``str``; an array of fixed-length strings stays a NumPy array of bytes.

    Args:
        path: Input file.
        backend: ``"h5py"``, ``"builtin"`` (pyfive), or ``"auto"`` for h5py when it is installed and pyfive
            otherwise.
        members: Names of the top-level members to read; the others are not read at all, which matters for a
            large file of which only a few columns are wanted. A name the file lacks is absent from the
            result. ``None``, the default, reads every member. The file's own attributes are always read.
        data: ``False`` reads the structure and the attributes only: every :class:`Dataset` comes back with
            ``data=None`` and its samples are never read, which matters for inspecting the metadata of a
            large file.

    Raises:
        ModuleNotFoundError: If ``backend="h5py"`` and h5py is not installed.
        TypeError: If ``members`` is a single string rather than a collection of names.
        ValueError: If a name in ``members`` is empty, ``.`` or ``..``, or contains ``/``: only top-level members
            can be selected, not paths into groups.
    """
    if isinstance(members, (str, bytes)):
        # A string is iterable, so it would be read as one-character names and silently select nothing.
        raise TypeError(f"members must be a collection of names, such as [{members!r}]; got a single string.")
    if members is not None:
        members = list(members)
        for name in members:
            _check_name(name)
    h5py = _h5py() if backend in ("auto", "h5py") else None
    if backend == "h5py" and h5py is None:
        raise ModuleNotFoundError(
            "The h5py backend needs h5py, which is not installed. Install it with: pip install 'gwmock-data[hdf5]'",
            name="h5py",
        )
    if h5py is not None:
        with h5py.File(Path(path), "r") as handle:
            return _read_tree(handle, lambda item: isinstance(item, h5py.Group), members, data=data)
    pyfive = importlib.import_module("pyfive")
    with pyfive.File(str(path)) as handle:
        return _read_tree(handle, lambda item: isinstance(item, pyfive.Group), members, data=data)

update_attrs(path, attrs, *, backend='auto')

Set attributes on members of an existing HDF5 file; the samples and every other attribute are kept.

With h5py the file is changed in place. Without it the whole file is read, the attributes merged, and the result written beside the file and moved over it once complete, so a failed update leaves the file as it was; the rewritten file keeps the original's permissions, and a symlink is written through to its target. The built-in writer does not compress and stores a string dataset fixed-length, so a file h5py wrote comes back uncompressed when it is updated without h5py, holding the same values.

Parameters:

Name Type Description Default
path str | Path

The file to change.

required
attrs Mapping[str, Mapping[str, Any]]

Attributes to set, keyed by the path of the member that carries them: "" for the file itself, "H1:STRAIN" for a top-level dataset, "group/dataset" below a group. An attribute already present is replaced; the others are kept.

required
backend Backend

"h5py", "builtin", or "auto" for h5py when it is installed and the built-in writer otherwise.

'auto'

Raises:

Type Description
FileNotFoundError

If there is no file at path.

KeyError

If a member path names nothing in the file; the file is not changed.

ModuleNotFoundError

If backend="h5py" and h5py is not installed.

TypeError

If the built-in writer cannot write a value the file holds, such as a half-precision float; the file is not changed.

ValueError

If a member path has an empty, . or .. component, or a string contains a NUL character.

Source code in src/gwmock_data/hdf5/__init__.py
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def update_attrs(path: str | Path, attrs: Mapping[str, Mapping[str, Any]], *, backend: Backend = "auto") -> None:
    """Set attributes on members of an existing HDF5 file; the samples and every other attribute are kept.

    With h5py the file is changed in place. Without it the whole file is read, the attributes merged, and the
    result written beside the file and moved over it once complete, so a failed update leaves the file as it
    was; the rewritten file keeps the original's permissions, and a symlink is written through to its target.
    The built-in writer does not compress and stores a string dataset fixed-length, so a file h5py wrote comes
    back uncompressed when it is updated without h5py, holding the same values.

    Args:
        path: The file to change.
        attrs: Attributes to set, keyed by the path of the member that carries them: ``""`` for the file
            itself, ``"H1:STRAIN"`` for a top-level dataset, ``"group/dataset"`` below a group. An attribute
            already present is replaced; the others are kept.
        backend: ``"h5py"``, ``"builtin"``, or ``"auto"`` for h5py when it is installed and the built-in writer
            otherwise.

    Raises:
        FileNotFoundError: If there is no file at ``path``.
        KeyError: If a member path names nothing in the file; the file is not changed.
        ModuleNotFoundError: If ``backend="h5py"`` and h5py is not installed.
        TypeError: If the built-in writer cannot write a value the file holds, such as a half-precision
            float; the file is not changed.
        ValueError: If a member path has an empty, ``.`` or ``..`` component, or a string contains a NUL
            character.
    """
    targets = {member: _member_parts(member) for member in attrs}
    # Every value is checked and every member found before anything is changed, so a bad value or a
    # missing member changes nothing.
    converted = {
        member: {key: _attribute(key, value) for key, value in values.items()} for member, values in attrs.items()
    }
    h5py = _h5py() if backend in ("auto", "h5py") else None
    if backend == "h5py" and h5py is None:
        raise ModuleNotFoundError(
            "The h5py backend needs h5py, which is not installed. Install it with: pip install 'gwmock-data[hdf5]'",
            name="h5py",
        )
    if h5py is not None:
        with h5py.File(Path(path), "r+") as handle:
            found = {}
            for member, parts in targets.items():
                if parts and "/".join(parts) not in handle:
                    raise KeyError(f"The file has no member {member!r}.")
                found[member] = handle["/".join(parts)] if parts else handle
            for member, values in converted.items():
                for key, value in values.items():
                    found[member].attrs[key] = value
        return
    root = read(path, backend="builtin")
    found_nodes = {member: _find(root, member, parts) for member, parts in targets.items()}
    for member, values in converted.items():
        found_nodes[member].attrs.update(values)
    destination = Path(path)
    target = destination.resolve() if destination.is_symlink() else destination
    partial = target.with_name(f".{target.name}.{uuid.uuid4().hex}.partial")
    try:
        write(root, partial, backend="builtin")
        shutil.copymode(target, partial)
        partial.replace(target)
    finally:
        partial.unlink(missing_ok=True)

write(root, path, *, backend='auto', compression=None)

Write root to path as an HDF5 file, replacing it.

Parameters:

Name Type Description Default
root Group

The file's contents.

required
path str | Path

Output file.

required
backend Backend

"h5py", "builtin", or "auto" for h5py when it is installed and the built-in writer otherwise.

'auto'
compression str | None

A compression filter for h5py to apply to non-scalar datasets, such as "gzip". The built-in writer does not compress and ignores it.

None

Raises:

Type Description
ModuleNotFoundError

If backend="h5py" and h5py is not installed.

TypeError

If a dtype cannot be written.

ValueError

If a member name cannot name an HDF5 member, or a string attribute contains a NUL character.

Source code in src/gwmock_data/hdf5/__init__.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def write(root: Group, path: str | Path, *, backend: Backend = "auto", compression: str | None = None) -> None:
    """Write ``root`` to ``path`` as an HDF5 file, replacing it.

    Args:
        root: The file's contents.
        path: Output file.
        backend: ``"h5py"``, ``"builtin"``, or ``"auto"`` for h5py when it is installed and the built-in writer
            otherwise.
        compression: A compression filter for h5py to apply to non-scalar datasets, such as ``"gzip"``. The
            built-in writer does not compress and ignores it.

    Raises:
        ModuleNotFoundError: If ``backend="h5py"`` and h5py is not installed.
        TypeError: If a dtype cannot be written.
        ValueError: If a member name cannot name an HDF5 member, or a string attribute contains a NUL character.
    """
    # Before choosing a backend, so that the same tree means the same file whichever is installed.
    normalised = _normalised(root)
    h5py = _h5py() if backend in ("auto", "h5py") else None
    if backend == "h5py" and h5py is None:
        raise ModuleNotFoundError(
            "The h5py backend needs h5py, which is not installed. Install it with: pip install 'gwmock-data[hdf5]'",
            name="h5py",
        )
    path = Path(path)
    if h5py is None:
        path.write_bytes(encode(normalised))
        return
    with h5py.File(path, "w") as handle:
        _write_h5py(handle, root, compression)