diff --git a/heudiconv/bids.py b/heudiconv/bids.py index ab32feb1..da396dbd 100644 --- a/heudiconv/bids.py +++ b/heudiconv/bids.py @@ -253,27 +253,13 @@ def populate_aggregated_jsons(path: str) -> None: # create a stub onsets file for each one of those suf = "_bold.json" assert fpath.endswith(suf) - # specify the name of the '_events.tsv' file: - if "_echo-" in fpath: - # multi-echo sequence: bids (1.1.0) specifies just one '_events.tsv' - # file, common for all echoes. The name will not include _echo-. - # TODO: RF to use re.match for better readability/robustness - # So, find out the echo number: - fpath_split = fpath.split("_echo-", 1) # split fpath using '_echo-' - fpath_split_2 = fpath_split[1].split( - "_", 1 - ) # split the second part of fpath_split using '_' - echoNo = fpath_split_2[0] # get echo number - if echoNo == "1": - if len(fpath_split_2) != 2: - raise ValueError("Found no trailer after _echo-") - # we modify fpath to exclude '_echo-' + echoNo: - fpath = fpath_split[0] + "_" + fpath_split_2[1] - else: - # for echoNo greater than 1, don't create the events file, so go to - # the next for loop iteration: - continue + # specify the name of the '_events.tsv' file: + parsed_fpath = BIDSFile.parse(op.basename(fpath)) + for events_invalid_entity in ['chunk', 'echo', 'part']: + # events.tsv with these entities are not specified + parsed_fpath.drop(events_invalid_entity, missing_ok=True) + fpath = op.join(op.dirname(fpath), str(parsed_fpath)) events_file = remove_suffix(fpath, suf) + "_events.tsv" # do not touch any existing thing, it may be precious if not op.lexists(events_file): @@ -1090,6 +1076,7 @@ class BIDSFile: "mt", "part", "recording", + "chunk", ] def __init__( @@ -1156,6 +1143,16 @@ def __setitem__( ) -> None: # would puke with some exception if already known return self.set(entity, value, overwrite=False) + def __contains__(self, entity: object) -> bool: + return entity in self._entities + + def drop(self, entity: str, missing_ok: bool=False) -> None: + if entity not in self._entities: + if not missing_ok: + raise ValueError(f"{self} does not contain entity {entity!r}") + return + self._entities.pop(entity) + def set(self, entity: str, value: str, overwrite: bool = True) -> None: if entity not in self._entities: # just set it; no complains here diff --git a/heudiconv/tests/test_bids.py b/heudiconv/tests/test_bids.py index 38ddd215..a733be7c 100644 --- a/heudiconv/tests/test_bids.py +++ b/heudiconv/tests/test_bids.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta from glob import glob import itertools +import logging import os import os.path as op from pathlib import Path @@ -31,6 +32,7 @@ get_key_info_for_fmap_assignment, get_shim_setting, maybe_na, + populate_aggregated_jsons, populate_intended_for, sanitize_label, select_fmap_from_compatible_groups, @@ -1506,7 +1508,7 @@ def test_populate_intended_for( assert "IntendedFor" not in data.keys() -def test_BIDSFile() -> None: +def test_BIDSFile(caplog: pytest.LogCaptureFixture) -> None: """Tests for the BIDSFile class""" # define entities in the correct order: @@ -1574,6 +1576,39 @@ def test_BIDSFile() -> None: my_bids_file.set("echo", "2") assert my_bids_file["echo"] == "2" + # Test drop method + my_bids_file.drop("dir") + assert "dir" not in my_bids_file + # dropping an entity which is not set only logs a warning + caplog.set_level(logging.WARNING) + # test previously dropped entity and entirely non-existing + for entity in ['dir', 'not_existing']: + with pytest.raises(ValueError, match=f"does not contain entity {entity!r}"): + my_bids_file.drop(entity) + # implicitly assert that no exception is thrown when using missing_ok + my_bids_file.drop(entity, missing_ok=True) + + +def test_populate_aggregated_jsons_events(tmp_path: Path) -> None: + """A single _events.tsv is generated for files differing only in + entities the events are independent of ('chunk', 'echo', and 'part') """ + func_path = tmp_path / "sub-01" / "func" + bold_json = {"RepetitionTime": 1.0, "TaskName": "rest"} + create_tree( + str(func_path), + { + f"sub-01_task-rest_{entity}_bold.json": dict(bold_json) + for entity in ["chunk-1", "chunk-2", "echo-1", "echo-2", "part-mag", "part-phase"] + }, + ) + + populate_aggregated_jsons(str(tmp_path)) + + events_files = sorted(func_path.glob("*_events.tsv")) + assert events_files == [func_path / "sub-01_task-rest_events.tsv"] + # and nothing got written elsewhere in the dataset: + assert sorted(tmp_path.rglob("*_events.tsv")) == events_files + @pytest.mark.skipif(not have_datalad, reason="no datalad") def test_ME_mag_phase_conversion(