Skip to content

Commit 42a4326

Browse files
committed
ENH: Adopt config module
Close #87
1 parent 7e4bce7 commit 42a4326

File tree

7 files changed

+1230
-585
lines changed

7 files changed

+1230
-585
lines changed

dmriprep/__init__.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
"""Top-level package for dmriprep."""
2-
import warnings as _warnings
32
from .__about__ import (
43
__version__,
54
__copyright__,
65
__credits__,
76
__packagename__,
87
)
98

10-
119
__all__ = [
1210
'__version__',
1311
'__copyright__',
1412
'__credits__',
1513
'__packagename__',
1614
]
17-
18-
# cmp is not used by dmriprep, so ignore nipype-generated warnings
19-
_warnings.filterwarnings('ignore', r'cmp not installed')
20-
_warnings.filterwarnings('ignore', r'This has not been fully tested. Please report any failures.')
21-
_warnings.filterwarnings('ignore', r"can't resolve package from __spec__ or __package__")
22-
_warnings.simplefilter('ignore', DeprecationWarning)
23-
_warnings.simplefilter('ignore', ResourceWarning)

dmriprep/cli/parser.py

Lines changed: 339 additions & 0 deletions
Large diffs are not rendered by default.

dmriprep/cli/run.py

Lines changed: 78 additions & 524 deletions
Large diffs are not rendered by default.

dmriprep/cli/tests/test_parser.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Test parser."""
2+
from packaging.version import Version
3+
import pytest
4+
from ..parser import _build_parser
5+
from .. import version as _version
6+
from ... import config
7+
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+
])
17+
def test_parser_errors(args, code):
18+
"""Check behavior of the parser."""
19+
with pytest.raises(SystemExit) as error:
20+
_build_parser().parse_args(args)
21+
22+
assert error.value.code == code
23+
24+
25+
@pytest.mark.parametrize('args', [
26+
MIN_ARGS,
27+
MIN_ARGS + ['--fs-license-file'],
28+
])
29+
def test_parser_valid(tmp_path, args):
30+
"""Check valid arguments."""
31+
datapath = (tmp_path / 'data')
32+
datapath.mkdir(exist_ok=True)
33+
args[0] = str(datapath)
34+
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()))
40+
41+
opts = _build_parser().parse_args(args)
42+
43+
assert opts.bids_dir == datapath
44+
45+
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+
])
60+
def test_memory_arg(tmp_path, argval, gb):
61+
"""Check the correct parsing of the memory argument."""
62+
datapath = (tmp_path / 'data')
63+
datapath.mkdir(exist_ok=True)
64+
_fs_file = tmp_path / 'license.txt'
65+
_fs_file.write_text('')
66+
67+
args = MIN_ARGS + ['--fs-license-file', str(_fs_file)] \
68+
+ ['--mem', argval]
69+
opts = _build_parser().parse_args(args)
70+
71+
assert opts.memory_gb == gb
72+
73+
74+
@pytest.mark.parametrize('current,latest', [
75+
('1.0.0', '1.3.2'),
76+
('1.3.2', '1.3.2')
77+
])
78+
def test_get_parser_update(monkeypatch, capsys, current, latest):
79+
"""Make sure the out-of-date banner is shown."""
80+
expectation = Version(current) < Version(latest)
81+
82+
def _mock_check_latest(*args, **kwargs):
83+
return Version(latest)
84+
85+
monkeypatch.setattr(config.environment, 'version', current)
86+
monkeypatch.setattr(_version, 'check_latest', _mock_check_latest)
87+
88+
_build_parser()
89+
captured = capsys.readouterr().err
90+
91+
msg = """\
92+
You are using dMRIPrep-%s, and a newer version of dMRIPrep is available: %s.
93+
Please check out our documentation about how and when to upgrade:
94+
https://dmriprep.readthedocs.io/en/latest/faq.html#upgrading""" % (current, latest)
95+
96+
assert (msg in captured) is expectation
97+
98+
99+
@pytest.mark.parametrize('flagged', [
100+
(True, None),
101+
(True, 'random reason'),
102+
(False, None),
103+
])
104+
def test_get_parser_blacklist(monkeypatch, capsys, flagged):
105+
"""Make sure the blacklisting banner is shown."""
106+
def _mock_is_bl(*args, **kwargs):
107+
return flagged
108+
109+
monkeypatch.setattr(_version, 'is_flagged', _mock_is_bl)
110+
111+
_build_parser()
112+
captured = capsys.readouterr().err
113+
114+
assert ('FLAGGED' in captured) is flagged[0]
115+
if flagged[0]:
116+
assert ((flagged[1] or 'reason: unknown') in captured)

dmriprep/cli/tests/test_run.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

dmriprep/cli/workflow.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
The workflow builder factory method.
3+
4+
All the checks and the construction of the workflow are done
5+
inside this function that has pickleable inputs and output
6+
dictionary (``retval``) to allow isolation using a
7+
``multiprocessing.Process`` that allows dmriprep to enforce
8+
a hard-limited memory-scope.
9+
10+
"""
11+
12+
13+
def build_workflow(config_file, retval):
14+
"""Create the Nipype Workflow that supports the whole execution graph."""
15+
from niworkflows.utils.bids import collect_participants, check_pipeline_version
16+
from niworkflows.reports import generate_reports
17+
from .. import config
18+
from ..utils.misc import check_deps
19+
from ..workflows.base import init_dmriprep_wf
20+
21+
config.load(config_file)
22+
build_log = config.loggers.workflow
23+
24+
output_dir = config.execution.output_dir
25+
version = config.environment.version
26+
27+
retval['return_code'] = 1
28+
retval['workflow'] = None
29+
30+
# warn if older results exist: check for dataset_description.json in output folder
31+
msg = check_pipeline_version(
32+
version, output_dir / 'dmriprep' / 'dataset_description.json'
33+
)
34+
if msg is not None:
35+
build_log.warning(msg)
36+
37+
# Please note this is the input folder's dataset_description.json
38+
dset_desc_path = config.execution.bids_dir / 'dataset_description.json'
39+
if dset_desc_path.exists():
40+
from hashlib import sha256
41+
desc_content = dset_desc_path.read_bytes()
42+
config.execution.bids_description_hash = sha256(desc_content).hexdigest()
43+
44+
# First check that bids_dir looks like a BIDS folder
45+
subject_list = collect_participants(
46+
config.execution.layout,
47+
participant_label=config.execution.participant_label
48+
)
49+
50+
# Called with reports only
51+
if config.execution.reports_only:
52+
from pkg_resources import resource_filename as pkgrf
53+
54+
build_log.log(25, 'Running --reports-only on participants %s', ', '.join(subject_list))
55+
retval['return_code'] = generate_reports(
56+
subject_list,
57+
config.execution.output_dir,
58+
config.execution.work_dir,
59+
config.execution.run_uuid,
60+
config=pkgrf('dmriprep', 'config/reports-spec.yml'),
61+
packagename='dmriprep')
62+
return retval
63+
64+
# Build main workflow
65+
INIT_MSG = """
66+
Running dMRIPREP version {version}:
67+
* BIDS dataset path: {bids_dir}.
68+
* Participant list: {subject_list}.
69+
* Run identifier: {uuid}.
70+
* Output spaces: {spaces}.
71+
""".format
72+
build_log.log(25, INIT_MSG(
73+
version=config.environment.version,
74+
bids_dir=config.execution.bids_dir,
75+
subject_list=subject_list,
76+
uuid=config.execution.run_uuid,
77+
spaces=config.execution.output_spaces)
78+
)
79+
80+
retval['workflow'] = init_dmriprep_wf()
81+
82+
# Check workflow for missing commands
83+
missing = check_deps(retval['workflow'])
84+
if missing:
85+
build_log.critical(
86+
"Cannot run dMRIPrep. Missing dependencies:%s",
87+
'\n\t* %s'.join(["{} (Interface: {})".format(cmd, iface)
88+
for iface, cmd in missing])
89+
)
90+
retval['return_code'] = 127 # 127 == command not found.
91+
return retval
92+
93+
config.to_filename(config_file)
94+
build_log.info(
95+
"dMRIPrep workflow graph with %d nodes built successfully.",
96+
len(retval['workflow']._get_all_nodes())
97+
)
98+
retval['return_code'] = 0
99+
return retval
100+
101+
102+
def build_boilerplate(config_file, workflow):
103+
"""Write boilerplate in an isolated process."""
104+
from .. import config
105+
106+
config.load(config_file)
107+
logs_path = config.execution.output_dir / 'dmriprep' / 'logs'
108+
boilerplate = workflow.visit_desc()
109+
citation_files = {
110+
ext: logs_path / ('CITATION.%s' % ext)
111+
for ext in ('bib', 'tex', 'md', 'html')
112+
}
113+
114+
if boilerplate:
115+
# To please git-annex users and also to guarantee consistency
116+
# among different renderings of the same file, first remove any
117+
# existing one
118+
for citation_file in citation_files.values():
119+
try:
120+
citation_file.unlink()
121+
except FileNotFoundError:
122+
pass
123+
124+
citation_files['md'].write_text(boilerplate)
125+
126+
if not config.execution.md_only_boilerplate and citation_files['md'].exists():
127+
from subprocess import check_call, CalledProcessError, TimeoutExpired
128+
from pkg_resources import resource_filename as pkgrf
129+
from shutil import copyfile
130+
# Generate HTML file resolving citations
131+
cmd = ['pandoc', '-s', '--bibliography',
132+
pkgrf('dmriprep', 'data/boilerplate.bib'),
133+
'--filter', 'pandoc-citeproc',
134+
'--metadata', 'pagetitle="dMRIPrep citation boilerplate"',
135+
str(citation_files['md']),
136+
'-o', str(citation_files['html'])]
137+
138+
config.loggers.cli.info(
139+
'Generating an HTML version of the citation boilerplate...')
140+
try:
141+
check_call(cmd, timeout=10)
142+
except (FileNotFoundError, CalledProcessError, TimeoutExpired):
143+
config.loggers.cli.warning(
144+
'Could not generate CITATION.html file:\n%s', ' '.join(cmd))
145+
146+
# Generate LaTex file resolving citations
147+
cmd = ['pandoc', '-s', '--bibliography',
148+
pkgrf('dmriprep', 'data/boilerplate.bib'),
149+
'--natbib', str(citation_files['md']),
150+
'-o', str(citation_files['tex'])]
151+
config.loggers.cli.info(
152+
'Generating a LaTeX version of the citation boilerplate...')
153+
try:
154+
check_call(cmd, timeout=10)
155+
except (FileNotFoundError, CalledProcessError, TimeoutExpired):
156+
config.loggers.cli.warning(
157+
'Could not generate CITATION.tex file:\n%s', ' '.join(cmd))
158+
else:
159+
copyfile(pkgrf('dmriprep', 'data/boilerplate.bib'),
160+
citation_files['bib'])

0 commit comments

Comments
 (0)