Skip to content

Commit 4516201

Browse files
authored
Merge pull request #1715 from oesteban/cli/version-checks
ENH: Add check for updates and check whether version has been flagged.
2 parents 9773f7e + 45eb4a3 commit 4516201

File tree

8 files changed

+338
-2
lines changed

8 files changed

+338
-2
lines changed

.versions.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"flagged": {
3+
"1.0.0": "Deprecated / too old"
4+
}
5+
}

docs/faq_tips_tricks.rst renamed to docs/faq.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,23 @@ A symptomatic output looks like: ::
126126

127127
If you would like to run *fMRIPrep* in parallel on multiple subjects please use
128128
`this method <https://neurostars.org/t/fmriprep-workaround-for-running-subjects-in-parallel/4449>`__.
129+
130+
131+
.. _upgrading:
132+
133+
A new version of *fMRIPrep* has been published, when should I upgrade?
134+
----------------------------------------------------------------------
135+
136+
We follow a philosophy of releasing very often, although the pace is slowing down
137+
with the maturation of the software.
138+
It is very likely that your version gets outdated over the extent of your study.
139+
If that is the case (an ongoing study), then we discourage changing versions.
140+
In other words, **the whole dataset should be processed with the same version (and
141+
same container build if they are being used) of *fMRIPrep*.**
142+
143+
On the other hand, if the project is about to start, then we strongly recommend
144+
using the latest version of the tool.
145+
146+
In any case, if you can find your release listed as *flagged* in `this file
147+
of our repo <https://github.com/poldracklab/fmriprep/blob/master/.versions.json>`__,
148+
then please update as soon as possible.

docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Contents
2121
sdc
2222
spaces
2323
outputs
24-
faq_tips_tricks
24+
faq
2525
contributors
2626
citing
2727
api/index.rst

fmriprep/cli/run.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ def get_parser():
4040
"""Build parser object"""
4141
from smriprep.cli.utils import ParseTemplates, output_space as _output_space
4242
from templateflow.api import templates
43+
from packaging.version import Version
4344
from ..__about__ import __version__
4445
from ..workflows.bold.resampling import NONSTANDARD_REFERENCES
46+
from .version import check_latest, is_flagged
4547

4648
verstr = 'fmriprep v{}'.format(__version__)
4749

@@ -274,6 +276,25 @@ def get_parser():
274276
g_other.add_argument('--sloppy', action='store_true', default=False,
275277
help='Use low-quality tools for speed - TESTING ONLY')
276278

279+
currentv = Version(__version__)
280+
latest = check_latest()
281+
282+
if latest is not None and currentv < latest:
283+
print("""\
284+
You are using fMRIPrep-%s, and a newer version of fMRIPrep is available: %s.
285+
Please check out our documentation about how and when to upgrade:
286+
https://fmriprep.readthedocs.io/en/latest/faq.html#upgrading""" % (
287+
__version__, latest), file=sys.stderr)
288+
289+
_blist = is_flagged()
290+
if _blist[0]:
291+
_reason = _blist[1] or 'unknown'
292+
print("""\
293+
WARNING: Version %s of fMRIPrep (current) has been FLAGGED
294+
(reason: %s).
295+
That means some severe flaw was found in it and we strongly
296+
discourage its usage.""" % (__version__, _reason), file=sys.stderr)
297+
277298
return parser
278299

279300

@@ -283,7 +304,6 @@ def main():
283304
from multiprocessing import set_start_method, Process, Manager
284305
from ..utils.bids import write_derivative_description, validate_input_dir
285306
set_start_method('forkserver')
286-
287307
warnings.showwarning = _warn_redirect
288308
opts = get_parser().parse_args()
289309

fmriprep/cli/tests/__init__.py

Whitespace-only changes.

fmriprep/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 fMRIPrep-%s, and a newer version of fMRIPrep is available: %s.
28+
Please check out our documentation about how and when to upgrade:
29+
https://fmriprep.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)

fmriprep/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' / 'fmriprep' / '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' / 'fmriprep' / '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

fmriprep/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' / 'fmriprep' / '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/fmriprep/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/fmriprep/master/.versions.json
64+
flagged = tuple()
65+
try:
66+
response = requests.get(url="""\
67+
https://raw.githubusercontent.com/poldracklab/fmriprep/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

0 commit comments

Comments
 (0)