Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 37 additions & 27 deletions src/legenddataflowscripts/utils/alias_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,56 @@


def convert_parents_to_structs(h5group):
"""Recursively annotate HDF5 parent groups with LGDO ``struct`` datatype attributes.
"""Annotate HDF5 parent groups with LGDO ``struct`` datatype attributes.

When a new alias (hard link) is created inside an HDF5 file the parent
groups may not carry the ``datatype`` attribute expected by the LGDO reader.
This function walks up the HDF5 group hierarchy from *h5group* and ensures
every ancestor group carries a ``datatype`` attribute of the form
``struct{child1,child2,…}``.

The root group is only updated if it already carries a ``datatype``; the
walk always stops there.

Parameters
----------
h5group : h5py.Group
Leaf group whose parent hierarchy should be annotated.
"""
child = h5group.name.split("/")[-1]
if h5group.parent.name != "/" and len(h5group.parent.attrs) == 0:
h5group.parent.attrs.update({"datatype": "struct{" + child + "}"})
elif len(h5group.parent.attrs) > 0:
datatype = h5group.parent.attrs["datatype"]
if not (datatype.startswith("struct{") and datatype.endswith("}")):
msg = (
f"cannot register alias {child!r}: parent group "
f"{h5group.parent.name!r} has non-struct datatype {datatype!r}"
# the walk must be iterative: in h5py the root group is its own parent, so
# recursing on ``h5group.parent`` never terminates once ``/`` is reached
while h5group.name != "/":
parent = h5group.parent
child = h5group.name.rsplit("/", 1)[-1]

if "datatype" not in parent.attrs:
# never invent a datatype on the file root
if parent.name == "/":
return
parent.attrs.update({"datatype": "struct{" + child + "}"})
else:
datatype = parent.attrs["datatype"]
if not (datatype.startswith("struct{") and datatype.endswith("}")):
msg = (
f"cannot register alias {child!r}: parent group "
f"{parent.name!r} has non-struct datatype {datatype!r}"
)
raise ValueError(msg)
# membership must be tested against the parsed field list, not by
# substring (e.g. 'raw' is a substring of 'raw_blind')
fields = [
field.strip()
for field in datatype[len("struct{") : -1].split(",")
if field.strip()
]
if child in fields:
# ancestors were annotated when this member was first added
return
parent.attrs.update(
{"datatype": "struct{" + ",".join([*fields, child]) + "}"}
)
raise ValueError(msg)
# membership must be tested against the parsed field list, not by
# substring (e.g. 'raw' is a substring of 'raw_blind')
fields = [
field.strip()
for field in datatype[len("struct{") : -1].split(",")
if field.strip()
]
if child in fields:
return
h5group.parent.attrs.update(
{"datatype": "struct{" + ",".join([*fields, child]) + "}"}
)
else:
return
convert_parents_to_structs(h5group.parent)
return

h5group = parent


def alias_table(file: str | Path, mapping: str):
Expand Down
28 changes: 28 additions & 0 deletions tests/test_utils_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,34 @@ def test_alias_table_membership(tmp_path):
assert f["det"].attrs["datatype"] == "struct{raw_blind,raw,raw2}"


def test_alias_table_annotated_root(tmp_path):
"""Aliasing into a new top-level group of a file whose root is a struct."""
lh5_file = tmp_path / "test.lh5"
with h5py.File(lh5_file, "w") as f:
for rawid, det in (("ch1027200", "V02160A"), ("ch1027201", "V02162B")):
grp = f.create_group(f"{rawid}/dsp")
grp.attrs["datatype"] = "table{a}"
grp.create_dataset("a", data=[1, 2, 3])
f[rawid].attrs["datatype"] = "struct{dsp}"
# real LH5 files always carry a datatype on the root group; the parent
# walk must stop there instead of recursing on it forever
f["/"].attrs["datatype"] = "struct{ch1027200,ch1027201}"

alias_table(lh5_file, {"ch1027200/dsp": "dsp/V02160A"})

with h5py.File(lh5_file) as f:
assert f["/"].attrs["datatype"] == "struct{ch1027200,ch1027201,dsp}"
assert f["dsp"].attrs["datatype"] == "struct{V02160A}"
assert list(f["dsp/V02160A/a"]) == [1, 2, 3]

# a second alias extends the new group but leaves the root untouched
alias_table(lh5_file, {"ch1027201/dsp": "dsp/V02162B"})

with h5py.File(lh5_file) as f:
assert f["/"].attrs["datatype"] == "struct{ch1027200,ch1027201,dsp}"
assert f["dsp"].attrs["datatype"] == "struct{V02160A,V02162B}"


def test_build_log_excepthook(tmp_path, monkeypatch):
monkeypatch.setattr(sys, "stderr", sys.stderr)
monkeypatch.setattr(sys, "excepthook", sys.excepthook)
Expand Down