Skip to content

Commit d07d4b0

Browse files
authored
Merge pull request #105 from oesteban/enh/refactor-base-dwi-workflow
MAINT: Refactor the workflow to use Nipype iterables
2 parents 9d95e73 + c03474e commit d07d4b0

File tree

22 files changed

+1039
-780
lines changed

22 files changed

+1039
-780
lines changed

dmriprep/cli/parser.py

Lines changed: 313 additions & 145 deletions
Large diffs are not rendered by default.

dmriprep/cli/run.py

Lines changed: 51 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,33 @@ def main():
1818
if not config.execution.notrack:
1919
import popylar
2020
from ..__about__ import __ga_id__
21+
2122
config.loggers.cli.info(
2223
"Your usage of dmriprep is being recorded using popylar (https://popylar.github.io/). ", # noqa
2324
"For details, see https://nipreps.github.io/dmriprep/usage.html. ",
24-
"To opt out, call dmriprep with a `--notrack` flag")
25-
popylar.track_event(__ga_id__, 'run', 'cli_run')
25+
"To opt out, call dmriprep with a `--notrack` flag",
26+
)
27+
popylar.track_event(__ga_id__, "run", "cli_run")
2628

2729
# CRITICAL Save the config to a file. This is necessary because the execution graph
2830
# is built as a separate process to keep the memory footprint low. The most
2931
# straightforward way to communicate with the child process is via the filesystem.
30-
config_file = config.execution.work_dir / '.dmriprep.toml'
32+
config_file = config.execution.work_dir / ".dmriprep.toml"
3133
config.to_filename(config_file)
3234

3335
# CRITICAL Call build_workflow(config_file, retval) in a subprocess.
3436
# Because Python on Linux does not ever free virtual memory (VM), running the
3537
# workflow construction jailed within a process preempts excessive VM buildup.
3638
with Manager() as mgr:
3739
from .workflow import build_workflow
40+
3841
retval = mgr.dict()
3942
p = Process(target=build_workflow, args=(str(config_file), retval))
4043
p.start()
4144
p.join()
4245

43-
retcode = p.exitcode or retval.get('return_code', 0)
44-
dmriprep_wf = retval.get('workflow', None)
46+
retcode = p.exitcode or retval.get("return_code", 0)
47+
dmriprep_wf = retval.get("workflow", None)
4548

4649
# CRITICAL Load the config from the file. This is necessary because the ``build_workflow``
4750
# function executed constrained in a process may change the config (and thus the global
@@ -52,7 +55,7 @@ def main():
5255
sys.exit(int(retcode > 0))
5356

5457
if dmriprep_wf and config.execution.write_graph:
55-
dmriprep_wf.write_graph(graph2use="colored", format='svg', simple_form=True)
58+
dmriprep_wf.write_graph(graph2use="colored", format="svg", simple_form=True)
5659

5760
retcode = retcode or (dmriprep_wf is None) * os.EX_SOFTWARE
5861
if retcode != 0:
@@ -61,8 +64,8 @@ def main():
6164
# Generate boilerplate
6265
with Manager() as mgr:
6366
from .workflow import build_boilerplate
64-
p = Process(target=build_boilerplate,
65-
args=(str(config_file), dmriprep_wf))
67+
68+
p = Process(target=build_boilerplate, args=(str(config_file), dmriprep_wf))
6669
p.start()
6770
p.join()
6871

@@ -73,39 +76,52 @@ def main():
7376
gc.collect()
7477

7578
if popylar is not None:
76-
popylar.track_event(__ga_id__, 'run', 'started')
79+
popylar.track_event(__ga_id__, "run", "started")
7780

78-
config.loggers.workflow.log(15, '\n'.join(
79-
['dMRIPrep config:'] + ['\t\t%s' % s for s in config.dumps().splitlines()])
81+
config.loggers.workflow.log(
82+
15,
83+
"\n".join(
84+
["dMRIPrep config:"] + ["\t\t%s" % s for s in config.dumps().splitlines()]
85+
),
8086
)
81-
config.loggers.workflow.log(25, 'dMRIPrep started!')
87+
config.loggers.workflow.log(25, "dMRIPrep started!")
8288
errno = 1 # Default is error exit unless otherwise set
8389
try:
8490
dmriprep_wf.run(**config.nipype.get_plugin())
8591
except Exception as e:
8692
if not config.execution.notrack:
87-
popylar.track_event(__ga_id__, 'run', 'error')
88-
config.loggers.workflow.critical('dMRIPrep failed: %s', e)
93+
popylar.track_event(__ga_id__, "run", "error")
94+
config.loggers.workflow.critical("dMRIPrep failed: %s", e)
8995
raise
9096
else:
91-
config.loggers.workflow.log(25, 'dMRIPrep finished successfully!')
97+
config.loggers.workflow.log(25, "dMRIPrep finished successfully!")
9298

9399
# Bother users with the boilerplate only iff the workflow went okay.
94-
if (config.execution.output_dir / 'dmriprep' / 'logs' / 'CITATION.md').exists():
100+
if (config.execution.output_dir / "dmriprep" / "logs" / "CITATION.md").exists():
95101
config.loggers.workflow.log(
96-
25, 'Works derived from this dMRIPrep execution should '
97-
'include the following boilerplate:\n\n%s',
98-
(config.execution.output_dir / 'dmriprep' / 'logs' / 'CITATION.md').read_text()
102+
25,
103+
"Works derived from this dMRIPrep execution should "
104+
"include the following boilerplate:\n\n%s",
105+
(
106+
config.execution.output_dir / "dmriprep" / "logs" / "CITATION.md"
107+
).read_text(),
99108
)
100109

101110
if config.workflow.run_reconall:
102111
from templateflow import api
103112
from niworkflows.utils.misc import _copy_any
104-
dseg_tsv = str(api.get('fsaverage', suffix='dseg', extension=['.tsv']))
105-
_copy_any(dseg_tsv,
106-
str(config.execution.output_dir / 'dmriprep' / 'desc-aseg_dseg.tsv'))
107-
_copy_any(dseg_tsv,
108-
str(config.execution.output_dir / 'dmriprep' / 'desc-aparcaseg_dseg.tsv'))
113+
114+
dseg_tsv = str(api.get("fsaverage", suffix="dseg", extension=[".tsv"]))
115+
_copy_any(
116+
dseg_tsv,
117+
str(config.execution.output_dir / "dmriprep" / "desc-aseg_dseg.tsv"),
118+
)
119+
_copy_any(
120+
dseg_tsv,
121+
str(
122+
config.execution.output_dir / "dmriprep" / "desc-aparcaseg_dseg.tsv"
123+
),
124+
)
109125
errno = 0
110126
finally:
111127
from niworkflows.reports import generate_reports
@@ -117,17 +133,20 @@ def main():
117133
config.execution.output_dir,
118134
config.execution.work_dir,
119135
config.execution.run_uuid,
120-
config=pkgrf('dmriprep', 'config/reports-spec.yml'),
121-
packagename='dmriprep')
136+
config=pkgrf("dmriprep", "config/reports-spec.yml"),
137+
packagename="dmriprep",
138+
)
122139
write_derivative_description(
123-
config.execution.bids_dir,
124-
config.execution.output_dir / 'dmriprep')
140+
config.execution.bids_dir, config.execution.output_dir / "dmriprep"
141+
)
125142

126143
if failed_reports and not config.execution.notrack:
127-
popylar.track_event(__ga_id__, 'run', 'reporting_error')
144+
popylar.track_event(__ga_id__, "run", "reporting_error")
128145
sys.exit(int((errno + failed_reports) > 0))
129146

130147

131-
if __name__ == '__main__':
132-
raise RuntimeError("dmriprep/cli/run.py should not be run directly;\n"
133-
"Please `pip install` dmriprep and use the `dmriprep` command")
148+
if __name__ == "__main__":
149+
raise RuntimeError(
150+
"dmriprep/cli/run.py should not be run directly;\n"
151+
"Please `pip install` dmriprep and use the `dmriprep` command"
152+
)

dmriprep/cli/tests/test_parser.py

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
from .. import version as _version
66
from ... import config
77

8-
MIN_ARGS = ['data/', 'out/', 'participant']
9-
10-
11-
@pytest.mark.parametrize('args,code', [
12-
([], 2),
13-
(MIN_ARGS, 2), # bids_dir does not exist
14-
(MIN_ARGS + ['--fs-license-file'], 2),
15-
(MIN_ARGS + ['--fs-license-file', 'fslicense.txt'], 2),
16-
])
8+
MIN_ARGS = ["data/", "out/", "participant"]
9+
10+
11+
@pytest.mark.parametrize(
12+
"args,code",
13+
[
14+
([], 2),
15+
(MIN_ARGS, 2), # bids_dir does not exist
16+
(MIN_ARGS + ["--fs-license-file"], 2),
17+
(MIN_ARGS + ["--fs-license-file", "fslicense.txt"], 2),
18+
],
19+
)
1720
def test_parser_errors(args, code):
1821
"""Check behavior of the parser."""
1922
with pytest.raises(SystemExit) as error:
@@ -22,96 +25,93 @@ def test_parser_errors(args, code):
2225
assert error.value.code == code
2326

2427

25-
@pytest.mark.parametrize('args', [
26-
MIN_ARGS,
27-
MIN_ARGS + ['--fs-license-file'],
28-
])
28+
@pytest.mark.parametrize("args", [MIN_ARGS, MIN_ARGS + ["--fs-license-file"]])
2929
def test_parser_valid(tmp_path, args):
3030
"""Check valid arguments."""
31-
datapath = (tmp_path / 'data').absolute()
31+
datapath = (tmp_path / "data").absolute()
3232
datapath.mkdir(exist_ok=True)
3333
args[0] = str(datapath)
3434

35-
if '--fs-license-file' in args:
36-
_fs_file = tmp_path / 'license.txt'
37-
_fs_file.write_text('')
38-
args.insert(args.index('--fs-license-file') + 1,
39-
str(_fs_file.absolute()))
35+
if "--fs-license-file" in args:
36+
_fs_file = tmp_path / "license.txt"
37+
_fs_file.write_text("")
38+
args.insert(args.index("--fs-license-file") + 1, str(_fs_file.absolute()))
4039

4140
opts = _build_parser().parse_args(args)
4241

4342
assert opts.bids_dir == datapath
4443

4544

46-
@pytest.mark.parametrize('argval,gb', [
47-
('1G', 1),
48-
('1GB', 1),
49-
('1000', 1), # Default units are MB
50-
('32000', 32), # Default units are MB
51-
('4000', 4), # Default units are MB
52-
('1000M', 1),
53-
('1000MB', 1),
54-
('1T', 1000),
55-
('1TB', 1000),
56-
('%dK' % 1e6, 1),
57-
('%dKB' % 1e6, 1),
58-
('%dB' % 1e9, 1),
59-
])
45+
@pytest.mark.parametrize(
46+
"argval,gb",
47+
[
48+
("1G", 1),
49+
("1GB", 1),
50+
("1000", 1), # Default units are MB
51+
("32000", 32), # Default units are MB
52+
("4000", 4), # Default units are MB
53+
("1000M", 1),
54+
("1000MB", 1),
55+
("1T", 1000),
56+
("1TB", 1000),
57+
("%dK" % 1e6, 1),
58+
("%dKB" % 1e6, 1),
59+
("%dB" % 1e9, 1),
60+
],
61+
)
6062
def test_memory_arg(tmp_path, argval, gb):
6163
"""Check the correct parsing of the memory argument."""
62-
datapath = (tmp_path / 'data').absolute()
64+
datapath = (tmp_path / "data").absolute()
6365
datapath.mkdir(exist_ok=True)
64-
_fs_file = tmp_path / 'license.txt'
65-
_fs_file.write_text('')
66+
_fs_file = tmp_path / "license.txt"
67+
_fs_file.write_text("")
6668

67-
args = MIN_ARGS + ['--fs-license-file', str(_fs_file)] \
68-
+ ['--mem', argval]
69+
args = MIN_ARGS + ["--fs-license-file", str(_fs_file)] + ["--mem", argval]
6970
args[0] = str(datapath)
7071
opts = _build_parser().parse_args(args)
7172

7273
assert opts.memory_gb == gb
7374

7475

75-
@pytest.mark.parametrize('current,latest', [
76-
('1.0.0', '1.3.2'),
77-
('1.3.2', '1.3.2')
78-
])
76+
@pytest.mark.parametrize("current,latest", [("1.0.0", "1.3.2"), ("1.3.2", "1.3.2")])
7977
def test_get_parser_update(monkeypatch, capsys, current, latest):
8078
"""Make sure the out-of-date banner is shown."""
8179
expectation = Version(current) < Version(latest)
8280

8381
def _mock_check_latest(*args, **kwargs):
8482
return Version(latest)
8583

86-
monkeypatch.setattr(config.environment, 'version', current)
87-
monkeypatch.setattr(_version, 'check_latest', _mock_check_latest)
84+
monkeypatch.setattr(config.environment, "version", current)
85+
monkeypatch.setattr(_version, "check_latest", _mock_check_latest)
8886

8987
_build_parser()
9088
captured = capsys.readouterr().err
9189

9290
msg = """\
9391
You are using dMRIPrep-%s, and a newer version of dMRIPrep is available: %s.
9492
Please check out our documentation about how and when to upgrade:
95-
https://dmriprep.readthedocs.io/en/latest/faq.html#upgrading""" % (current, latest)
93+
https://dmriprep.readthedocs.io/en/latest/faq.html#upgrading""" % (
94+
current,
95+
latest,
96+
)
9697

9798
assert (msg in captured) is expectation
9899

99100

100-
@pytest.mark.parametrize('flagged', [
101-
(True, None),
102-
(True, 'random reason'),
103-
(False, None),
104-
])
101+
@pytest.mark.parametrize(
102+
"flagged", [(True, None), (True, "random reason"), (False, None)]
103+
)
105104
def test_get_parser_blacklist(monkeypatch, capsys, flagged):
106105
"""Make sure the blacklisting banner is shown."""
106+
107107
def _mock_is_bl(*args, **kwargs):
108108
return flagged
109109

110-
monkeypatch.setattr(_version, 'is_flagged', _mock_is_bl)
110+
monkeypatch.setattr(_version, "is_flagged", _mock_is_bl)
111111

112112
_build_parser()
113113
captured = capsys.readouterr().err
114114

115-
assert ('FLAGGED' in captured) is flagged[0]
115+
assert ("FLAGGED" in captured) is flagged[0]
116116
if flagged[0]:
117-
assert ((flagged[1] or 'reason: unknown') in captured)
117+
assert (flagged[1] or "reason: unknown") in captured

0 commit comments

Comments
 (0)