Skip to content

Commit dc9b5d6

Browse files
committed
enh(packaging): Infrastructure to run like fMRIPrep
1 parent 4726a4a commit dc9b5d6

File tree

9 files changed

+1028
-9
lines changed

9 files changed

+1028
-9
lines changed

dmriprep/cli/__init__.py

Whitespace-only changes.

dmriprep/cli/run.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def get_parser():
3737
from templateflow.api import templates
3838
from packaging.version import Version
3939
from ..__about__ import __version__
40-
from ..workflows.bold.resampling import NONSTANDARD_REFERENCES
40+
from ..config import NONSTANDARD_REFERENCES
4141
from .version import check_latest, is_flagged
4242

4343
verstr = 'dmriprep v{}'.format(__version__)
@@ -280,7 +280,7 @@ def main():
280280
work_dir = Path(retval.get('work_dir'))
281281
plugin_settings = retval.get('plugin_settings', None)
282282
subject_list = retval.get('subject_list', None)
283-
fmriprep_wf = retval.get('workflow', None)
283+
dmriprep_wf = retval.get('workflow', None)
284284
run_uuid = retval.get('run_uuid', None)
285285

286286
if opts.reports_only:
@@ -289,15 +289,15 @@ def main():
289289
if opts.boilerplate:
290290
sys.exit(int(retcode > 0))
291291

292-
if fmriprep_wf and opts.write_graph:
293-
fmriprep_wf.write_graph(graph2use="colored", format='svg', simple_form=True)
292+
if dmriprep_wf and opts.write_graph:
293+
dmriprep_wf.write_graph(graph2use="colored", format='svg', simple_form=True)
294294

295-
retcode = retcode or int(fmriprep_wf is None)
295+
retcode = retcode or int(dmriprep_wf is None)
296296
if retcode != 0:
297297
sys.exit(retcode)
298298

299299
# Check workflow for missing commands
300-
missing = check_deps(fmriprep_wf)
300+
missing = check_deps(dmriprep_wf)
301301
if missing:
302302
print("Cannot run dMRIPrep. Missing dependencies:", file=sys.stderr)
303303
for iface, cmd in missing:
@@ -313,7 +313,7 @@ def main():
313313

314314
errno = 1 # Default is error exit unless otherwise set
315315
try:
316-
fmriprep_wf.run(**plugin_settings)
316+
dmriprep_wf.run(**plugin_settings)
317317
except Exception as e:
318318
if not opts.notrack:
319319
from ..utils.sentry import process_crashfile
@@ -417,7 +417,7 @@ def build_workflow(opts, retval):
417417
from niworkflows.utils.bids import collect_participants
418418
from niworkflows.reports import generate_reports
419419
from ..__about__ import __version__
420-
from ..workflows.base import init_fmriprep_wf
420+
from ..workflows.base import init_dmriprep_wf
421421

422422
build_log = nlogging.getLogger('nipype.workflow')
423423

@@ -546,7 +546,7 @@ def build_workflow(opts, retval):
546546
uuid=run_uuid)
547547
)
548548

549-
retval['workflow'] = init_fmriprep_wf(
549+
retval['workflow'] = init_dmriprep_wf(
550550
anat_only=opts.anat_only,
551551
aroma_melodic_dim=opts.aroma_melodic_dimensionality,
552552
bold2t1w_dof=opts.bold2t1w_dof,

dmriprep/cli/tests/__init__.py

Whitespace-only changes.

dmriprep/cli/tests/test_run.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Test CLI."""
2+
from packaging.version import Version
3+
import pytest
4+
from .. import version as _version
5+
from ... import __about__
6+
from ..run import get_parser
7+
8+
9+
@pytest.mark.parametrize(('current', 'latest'), [
10+
('1.0.0', '1.3.2'),
11+
('1.3.2', '1.3.2')
12+
])
13+
def test_get_parser_update(monkeypatch, capsys, current, latest):
14+
"""Make sure the out-of-date banner is shown."""
15+
expectation = Version(current) < Version(latest)
16+
17+
def _mock_check_latest(*args, **kwargs):
18+
return Version(latest)
19+
20+
monkeypatch.setattr(__about__, '__version__', current)
21+
monkeypatch.setattr(_version, 'check_latest', _mock_check_latest)
22+
23+
get_parser()
24+
captured = capsys.readouterr().err
25+
26+
msg = """\
27+
You are using dMRIPrep-%s, and a newer version of dMRIPrep is available: %s.
28+
Please check out our documentation about how and when to upgrade:
29+
https://dmriprep.readthedocs.io/en/latest/faq.html#upgrading""" % (current, latest)
30+
31+
assert (msg in captured) is expectation
32+
33+
34+
@pytest.mark.parametrize('flagged', [
35+
(True, None),
36+
(True, 'random reason'),
37+
(False, None),
38+
])
39+
def test_get_parser_blacklist(monkeypatch, capsys, flagged):
40+
"""Make sure the blacklisting banner is shown."""
41+
def _mock_is_bl(*args, **kwargs):
42+
return flagged
43+
44+
monkeypatch.setattr(_version, 'is_flagged', _mock_is_bl)
45+
46+
get_parser()
47+
captured = capsys.readouterr().err
48+
49+
assert ('FLAGGED' in captured) is flagged[0]
50+
if flagged[0]:
51+
assert ((flagged[1] or 'reason: unknown') in captured)

dmriprep/cli/tests/test_version.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Test version checks."""
2+
from datetime import datetime
3+
from pathlib import Path
4+
from packaging.version import Version
5+
import pytest
6+
from .. import version as _version
7+
from ..version import check_latest, DATE_FMT, requests, is_flagged
8+
9+
10+
class MockResponse:
11+
"""Mocks the requests module so that Pypi is not actually queried."""
12+
13+
status_code = 200
14+
_json = {
15+
"releases": {
16+
'1.0.0': None,
17+
'1.0.1': None,
18+
'1.1.0': None,
19+
'1.1.1rc1': None
20+
}
21+
}
22+
23+
def __init__(self, code=200, json=None):
24+
"""Allow setting different response codes."""
25+
self.status_code = code
26+
if json is not None:
27+
self._json = json
28+
29+
def json(self):
30+
"""Redefine the response object."""
31+
return self._json
32+
33+
34+
def test_check_latest1(tmpdir, monkeypatch):
35+
"""Test latest version check."""
36+
tmpdir.chdir()
37+
monkeypatch.setenv('HOME', str(tmpdir))
38+
assert str(Path.home()) == str(tmpdir)
39+
40+
def mock_get(*args, **kwargs):
41+
return MockResponse()
42+
monkeypatch.setattr(requests, "get", mock_get)
43+
44+
# Initially, cache should not exist
45+
cachefile = Path.home() / '.cache' / 'dmriprep' / 'latest'
46+
assert not cachefile.exists()
47+
48+
# First check actually fetches from pypi
49+
v = check_latest()
50+
assert cachefile.exists()
51+
assert isinstance(v, Version)
52+
assert v == Version('1.1.0')
53+
assert cachefile.read_text().split('|') == [str(v), datetime.now().strftime(DATE_FMT)]
54+
55+
# Second check - test the cache file is read
56+
cachefile.write_text('|'.join(('1.0.0', cachefile.read_text().split('|')[1])))
57+
v = check_latest()
58+
assert isinstance(v, Version)
59+
assert v == Version('1.0.0')
60+
61+
# Third check - forced oudating of cache
62+
cachefile.write_text('2.0.0|20180121')
63+
v = check_latest()
64+
assert isinstance(v, Version)
65+
assert v == Version('1.1.0')
66+
67+
# Mock timeouts
68+
def mock_get(*args, **kwargs):
69+
raise requests.exceptions.Timeout
70+
monkeypatch.setattr(requests, "get", mock_get)
71+
72+
cachefile.write_text('|'.join(('1.0.0', cachefile.read_text().split('|')[1])))
73+
v = check_latest()
74+
assert isinstance(v, Version)
75+
assert v == Version('1.0.0')
76+
77+
cachefile.write_text('2.0.0|20180121')
78+
v = check_latest()
79+
assert v is None
80+
81+
cachefile.unlink()
82+
v = check_latest()
83+
assert v is None
84+
85+
86+
@pytest.mark.parametrize(('result', 'code', 'json'), [
87+
(None, 404, None),
88+
(None, 200, {'releases': {'1.0.0rc1': None}}),
89+
(Version('1.1.0'), 200, None),
90+
(Version('1.0.0'), 200, {'releases': {'1.0.0': None}}),
91+
])
92+
def test_check_latest2(tmpdir, monkeypatch, result, code, json):
93+
"""Test latest version check with varying server responses."""
94+
tmpdir.chdir()
95+
monkeypatch.setenv('HOME', str(tmpdir))
96+
assert str(Path.home()) == str(tmpdir)
97+
98+
def mock_get(*args, **kwargs):
99+
return MockResponse(code=code, json=json)
100+
monkeypatch.setattr(requests, "get", mock_get)
101+
102+
v = check_latest()
103+
if result is None:
104+
assert v is None
105+
else:
106+
assert isinstance(v, Version)
107+
assert v == result
108+
109+
110+
@pytest.mark.parametrize('bad_cache', [
111+
'3laj#r???d|3akajdf#',
112+
'2.0.0|3akajdf#',
113+
'|'.join(('2.0.0', datetime.now().strftime(DATE_FMT), '')),
114+
''
115+
])
116+
def test_check_latest3(tmpdir, monkeypatch, bad_cache):
117+
"""Test latest version check when the cache file is corrupted."""
118+
tmpdir.chdir()
119+
monkeypatch.setenv('HOME', str(tmpdir))
120+
assert str(Path.home()) == str(tmpdir)
121+
122+
def mock_get(*args, **kwargs):
123+
return MockResponse()
124+
monkeypatch.setattr(requests, "get", mock_get)
125+
126+
# Initially, cache should not exist
127+
cachefile = Path.home() / '.cache' / 'dmriprep' / 'latest'
128+
cachefile.parent.mkdir(parents=True, exist_ok=True)
129+
assert not cachefile.exists()
130+
131+
cachefile.write_text(bad_cache)
132+
v = check_latest()
133+
assert isinstance(v, Version)
134+
assert v == Version('1.1.0')
135+
136+
137+
@pytest.mark.parametrize(('result', 'version', 'code', 'json'), [
138+
(False, '1.2.1', 200, {'flagged': {'1.0.0': None}}),
139+
(True, '1.2.1', 200, {'flagged': {'1.2.1': None}}),
140+
(True, '1.2.1', 200, {'flagged': {'1.2.1': 'FATAL Bug!'}}),
141+
(False, '1.2.1', 404, {'flagged': {'1.0.0': None}}),
142+
(False, '1.2.1', 200, {'flagged': []}),
143+
(False, '1.2.1', 200, {}),
144+
])
145+
def test_is_flagged(monkeypatch, result, version, code, json):
146+
"""Test that the flagged-versions check is correct."""
147+
monkeypatch.setattr(_version, "__version__", version)
148+
149+
def mock_get(*args, **kwargs):
150+
return MockResponse(code=code, json=json)
151+
monkeypatch.setattr(requests, "get", mock_get)
152+
153+
val, reason = is_flagged()
154+
assert val is result
155+
156+
test_reason = None
157+
if val:
158+
test_reason = json.get('flagged', {}).get(version, None)
159+
160+
if test_reason is not None:
161+
assert reason == test_reason
162+
else:
163+
assert reason is None

dmriprep/cli/version.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Version CLI helpers."""
4+
5+
from pathlib import Path
6+
from datetime import datetime
7+
import requests
8+
from .. import __version__
9+
10+
RELEASE_EXPIRY_DAYS = 14
11+
DATE_FMT = '%Y%m%d'
12+
13+
14+
def check_latest():
15+
"""Determine whether this is the latest version."""
16+
from packaging.version import Version, InvalidVersion
17+
18+
latest = None
19+
date = None
20+
outdated = None
21+
cachefile = Path.home() / '.cache' / 'dmriprep' / 'latest'
22+
cachefile.parent.mkdir(parents=True, exist_ok=True)
23+
24+
try:
25+
latest, date = cachefile.read_text().split('|')
26+
except Exception:
27+
pass
28+
else:
29+
try:
30+
latest = Version(latest)
31+
date = datetime.strptime(date, DATE_FMT)
32+
except (InvalidVersion, ValueError):
33+
latest = None
34+
else:
35+
if abs((datetime.now() - date).days) > RELEASE_EXPIRY_DAYS:
36+
outdated = True
37+
38+
if latest is None or outdated is True:
39+
try:
40+
response = requests.get(url='https://pypi.org/pypi/dmriprep/json', timeout=1.0)
41+
except Exception:
42+
response = None
43+
44+
if response and response.status_code == 200:
45+
versions = [Version(rel) for rel in response.json()['releases'].keys()]
46+
versions = [rel for rel in versions if not rel.is_prerelease]
47+
if versions:
48+
latest = sorted(versions)[-1]
49+
else:
50+
latest = None
51+
52+
if latest is not None:
53+
try:
54+
cachefile.write_text('|'.join(('%s' % latest, datetime.now().strftime(DATE_FMT))))
55+
except Exception:
56+
pass
57+
58+
return latest
59+
60+
61+
def is_flagged():
62+
"""Check whether current version is flagged."""
63+
# https://raw.githubusercontent.com/poldracklab/dmriprep/master/.versions.json
64+
flagged = tuple()
65+
try:
66+
response = requests.get(url="""\
67+
https://raw.githubusercontent.com/poldracklab/dmriprep/master/.versions.json""", timeout=1.0)
68+
except Exception:
69+
response = None
70+
71+
if response and response.status_code == 200:
72+
flagged = response.json().get('flagged', {}) or {}
73+
74+
if __version__ in flagged:
75+
return True, flagged[__version__]
76+
77+
return False, None

dmriprep/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
"""Settings."""
4+
5+
NONSTANDARD_REFERENCES = ['anat', 'T1w', 'dwi', 'fsnative']

0 commit comments

Comments
 (0)