diff --git a/fmriprep/cli/parser.py b/fmriprep/cli/parser.py index 195706610..61922b6ee 100644 --- a/fmriprep/cli/parser.py +++ b/fmriprep/cli/parser.py @@ -62,6 +62,23 @@ def __call__(self, parser, namespace, values, option_string=None): print(msg, file=sys.stderr) delattr(namespace, self.dest) + class ToDict(Action): + def __call__(self, parser, namespace, values, option_string=None): + d = {} + for spec in values: + try: + name, loc = spec.split('=') + loc = Path(loc) + except ValueError: + loc = Path(spec) + name = loc.name + + if name in d: + raise ValueError(f'Received duplicate derivative name: {name}') + + d[name] = loc + setattr(namespace, self.dest, d) + def _path_exists(path, parser): """Ensure a given path exists.""" if path is None or not Path(path).exists(): @@ -222,11 +239,15 @@ def _slice_time_ref(value, parser): g_bids.add_argument( '-d', '--derivatives', - action='store', - metavar='PATH', - type=Path, - nargs='*', - help='Search PATH(s) for pre-computed derivatives.', + action=ToDict, + metavar='PACKAGE=PATH', + type=str, + nargs='+', + help=( + 'Search PATH(s) for pre-computed derivatives. ' + 'These may be provided as named folders ' + '(e.g., `--derivatives smriprep=/path/to/smriprep`).' + ), ) g_bids.add_argument( '--bids-database-dir', diff --git a/fmriprep/cli/tests/test_parser.py b/fmriprep/cli/tests/test_parser.py index 371fe7fb5..ce84fb60f 100644 --- a/fmriprep/cli/tests/test_parser.py +++ b/fmriprep/cli/tests/test_parser.py @@ -228,3 +228,45 @@ def test_use_syn_sdc(tmp_path, args, expectation): assert opts.use_syn_sdc == expectation _reset_config() + + +def test_derivatives(tmp_path): + """Check the correct parsing of the derivatives argument.""" + bids_path = tmp_path / 'data' + out_path = tmp_path / 'out' + args = [str(bids_path), str(out_path), 'participant'] + bids_path.mkdir() + + parser = _build_parser() + + # Providing --derivatives without a path should raise an error + temp_args = args + ['--derivatives'] + with pytest.raises((SystemExit, ArgumentError)): + parser.parse_args(temp_args) + _reset_config() + + # Providing --derivatives without names should automatically label them + temp_args = args + ['--derivatives', str(bids_path / 'derivatives/smriprep')] + opts = parser.parse_args(temp_args) + assert opts.derivatives == {'smriprep': bids_path / 'derivatives/smriprep'} + _reset_config() + + # Providing --derivatives with names should use them + temp_args = args + [ + '--derivatives', + f'anat={str(bids_path / "derivatives/smriprep")}', + ] + opts = parser.parse_args(temp_args) + assert opts.derivatives == {'anat': bids_path / 'derivatives/smriprep'} + _reset_config() + + # Providing multiple unlabeled derivatives with the same name should raise an error + temp_args = args + [ + '--derivatives', + str(bids_path / 'derivatives_01/smriprep'), + str(bids_path / 'derivatives_02/smriprep'), + ] + with pytest.raises(ValueError, match='Received duplicate derivative name'): + parser.parse_args(temp_args) + + _reset_config() diff --git a/fmriprep/cli/workflow.py b/fmriprep/cli/workflow.py index 89310590b..5d62545e6 100644 --- a/fmriprep/cli/workflow.py +++ b/fmriprep/cli/workflow.py @@ -116,7 +116,7 @@ def build_workflow(config_file, retval): ] if config.execution.derivatives: - init_msg += [f'Searching for derivatives: {config.execution.derivatives}.'] + init_msg += [f'Searching for derivatives: {list(config.execution.derivatives.values())}.'] if config.execution.fs_subjects_dir: init_msg += [f"Pre-run FreeSurfer's SUBJECTS_DIR: {config.execution.fs_subjects_dir}."] diff --git a/fmriprep/config.py b/fmriprep/config.py index 08fbb51ce..4ba1ead2e 100644 --- a/fmriprep/config.py +++ b/fmriprep/config.py @@ -380,7 +380,7 @@ class execution(_Config): bids_dir = None """An existing path to the dataset, which must be BIDS-compliant.""" - derivatives = [] + derivatives = {} """Path(s) to search for pre-computed derivatives""" bids_database_dir = None """Path to the directory containing SQLite database indices for the input BIDS dataset.""" @@ -526,8 +526,8 @@ def _process_value(value): cls.bids_filters[acq][k] = _process_value(v) dataset_links = {'raw': cls.bids_dir} - for i_deriv, deriv_path in enumerate(cls.derivatives): - dataset_links[f'deriv-{i_deriv}'] = deriv_path + for deriv_name, deriv_path in cls.derivatives.items(): + dataset_links[deriv_name] = deriv_path cls.dataset_links = dataset_links if 'all' in cls.debug: diff --git a/fmriprep/workflows/base.py b/fmriprep/workflows/base.py index df3268a5d..d7387b5ee 100644 --- a/fmriprep/workflows/base.py +++ b/fmriprep/workflows/base.py @@ -249,7 +249,7 @@ def init_single_subject_wf(subject_id: str): std_spaces = spaces.get_spaces(nonstandard=False, dim=(3,)) std_spaces.append('fsnative') - for deriv_dir in config.execution.derivatives: + for deriv_dir in config.execution.derivatives.values(): anatomical_cache.update( collect_anat_derivatives( derivatives_dir=deriv_dir, @@ -653,7 +653,7 @@ def init_single_subject_wf(subject_id: str): entities = extract_entities(bold_series) - for deriv_dir in config.execution.derivatives: + for deriv_dir in config.execution.derivatives.values(): functional_cache.update( collect_derivatives( derivatives_dir=deriv_dir,