Skip to content

Commit 9cb2961

Browse files
committed
Merge branch 'master' of https://github.com/nipy/heudiconv into fix/group
2 parents eaafb3b + dcc590d commit 9cb2961

File tree

9 files changed

+134
-19
lines changed

9 files changed

+134
-19
lines changed

heudiconv/bids.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,27 @@ def add_participant_record(studydir, subject, age, sex):
240240
known_subjects = {l.split('\t')[0] for l in f.readlines()}
241241
if participant_id in known_subjects:
242242
return
243+
else:
244+
# Populate particpants.json (an optional file to describe column names in
245+
# participant.tsv). This auto generation will make BIDS-validator happy.
246+
participants_json = op.join(studydir, 'participants.json')
247+
if not op.lexists(participants_json):
248+
save_json(participants_json,
249+
OrderedDict([
250+
("participant_id", OrderedDict([
251+
("Description", "Participant identifier")])),
252+
("age", OrderedDict([
253+
("Description", "Age in years (TODO - verify) as in the initial"
254+
" session, might not be correct for other sessions")])),
255+
("sex", OrderedDict([
256+
("Description", "self-rated by participant, M for male/F for "
257+
"female (TODO: verify)")])),
258+
("group", OrderedDict([
259+
("Description", "(TODO: adjust - by default everyone is in "
260+
"control group)")])),
261+
]),
262+
sort_keys=False,
263+
indent=2)
243264
# Add a new participant
244265
with open(participants_tsv, 'a') as f:
245266
f.write(
@@ -311,7 +332,8 @@ def save_scans_key(item, bids_files):
311332

312333
def add_rows_to_scans_keys_file(fn, newrows):
313334
"""
314-
Add new rows to file fn for scans key filename
335+
Add new rows to file fn for scans key filename and generate accompanying json
336+
descriptor to make BIDS validator happy.
315337
316338
Parameters
317339
----------
@@ -334,6 +356,25 @@ def add_rows_to_scans_keys_file(fn, newrows):
334356
os.unlink(fn)
335357
else:
336358
fnames2info = newrows
359+
# Populate _scans.json (an optional file to describe column names in
360+
# _scans.tsv). This auto generation will make BIDS-validator happy.
361+
scans_json = '.'.join(fn.split('.')[:-1] + ['json'])
362+
if not op.lexists(scans_json):
363+
save_json(scans_json,
364+
OrderedDict([
365+
("filename", OrderedDict([
366+
("Description", "Name of the nifti file")])),
367+
("acq_time", OrderedDict([
368+
("LongName", "Acquisition time"),
369+
("Description", "Acquisition time of the particular scan")])),
370+
("operator", OrderedDict([
371+
("Description", "Name of the operator")])),
372+
("randstr", OrderedDict([
373+
("LongName", "Random string"),
374+
("Description", "md5 hash of UIDs")])),
375+
]),
376+
sort_keys=False,
377+
indent=2)
337378

338379
header = ['filename', 'acq_time', 'operator', 'randstr']
339380
# prepare all the data rows
@@ -370,7 +411,7 @@ def get_formatted_scans_key_row(dcm_fn):
370411
time = dcm_data.ContentTime.split('.')[0]
371412
td = time + date
372413
acq_time = datetime.strptime(td, '%H%M%S%Y%m%d').isoformat()
373-
except AttributeError as exc:
414+
except (AttributeError, ValueError) as exc:
374415
lgr.warning("Failed to get date/time for the content: %s", str(exc))
375416
acq_time = None
376417
# add random string

heudiconv/cli/run.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
lgr = logging.getLogger(__name__)
1818

19-
INIT_MSG = "Running {packname} version {version}".format
19+
INIT_MSG = "Running {packname} version {version} latest {latest}".format
2020

2121

2222
def is_interactive():
@@ -139,8 +139,11 @@ def get_parser():
139139
'(can be compressed) are supported in addition to '
140140
'directory. All matching tarballs for a subject are '
141141
'extracted and their content processed in a single '
142-
'pass. Note that you might need to surround the value '
143-
'with quotes to avoid {...} being considered by shell')
142+
'pass. If multiple tarballs are found, each is '
143+
'assumed to be a separate session and the --ses '
144+
'argument is ignored. Note that you might need to '
145+
'surround the value with quotes to avoid {...} being '
146+
'considered by shell')
144147
group.add_argument('--files', nargs='*',
145148
help='Files (tarballs, dicoms) or directories '
146149
'containing files to process. Cannot be provided if '
@@ -243,8 +246,16 @@ def process_args(args):
243246

244247
outdir = op.abspath(args.outdir)
245248

249+
import etelemetry
250+
try:
251+
latest = etelemetry.get_project("nipy/heudiconv")
252+
except Exception as e:
253+
lgr.warning("Could not check for version updates: ", e)
254+
latest = {"version": 'Unknown'}
255+
246256
lgr.info(INIT_MSG(packname=__packagename__,
247-
version=__version__))
257+
version=__version__,
258+
latest=latest["version"]))
248259

249260
if args.command:
250261
process_extra_commands(outdir, args)

heudiconv/convert.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -519,11 +519,11 @@ def save_converted_files(res, item_dicoms, bids_options, outtype, prefix, outnam
519519
# echo times for all bids_files and see if they are all the same or not.
520520

521521
# Check for varying echo times
522-
echo_times = set(
522+
echo_times = sorted(list(set(
523523
load_json(b).get('EchoTime', None)
524524
for b in bids_files
525525
if b
526-
)
526+
)))
527527

528528
is_multiecho = len(echo_times) > 1
529529

@@ -575,11 +575,13 @@ def save_converted_files(res, item_dicoms, bids_options, outtype, prefix, outnam
575575
# For multi-echo sequences, we have to specify the echo number in
576576
# the file name:
577577
if bids_file and is_multiecho:
578-
# Get the EchoNumber from json file info. If not present, it's echo-1
579-
echo_number = fileinfo.get('EchoNumber', 1)
580-
578+
# Get the EchoNumber from json file info. If not present, use EchoTime
579+
if 'EchoNumber' in fileinfo.keys():
580+
echo_number = fileinfo['EchoNumber']
581+
else:
582+
echo_number = echo_times.index(fileinfo['EchoTime']) + 1
581583

582-
supported_multiecho = ['_bold', '_epi', '_sbref', '_T1w', '_PDT2']
584+
supported_multiecho = ['_bold', '_phase', '_epi', '_sbref', '_T1w', '_PDT2']
583585
# Now, decide where to insert it.
584586
# Insert it **before** the following string(s), whichever appears first.
585587
for imgtype in supported_multiecho:

heudiconv/info.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@
88
uses the dcmstack package and dcm2niix tool to convert DICOM directories or
99
tarballs into collections of NIfTI files following pre-defined heuristic(s)."""
1010

11+
CLASSIFIERS = [
12+
'Environment :: Console',
13+
'Intended Audience :: Science/Research',
14+
'License :: OSI Approved :: Apache Software License',
15+
'Programming Language :: Python :: 2.7',
16+
'Programming Language :: Python :: 3.5',
17+
'Programming Language :: Python :: 3.6',
18+
'Programming Language :: Python :: 3.7',
19+
'Topic :: Scientific/Engineering'
20+
]
21+
22+
PYTHON_REQUIRES = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*"
23+
1124
REQUIRES = [
1225
'nibabel',
1326
'pydicom',
14-
'nipype>=1.0.0',
27+
'nipype >=1.0.0; python_version > "3.0"',
28+
'nipype >=1.0.0,!=1.2.1,!=1.2.2; python_version == "2.7"',
1529
'pathlib',
1630
'dcmstack>=0.7',
31+
'etelemetry',
1732
]
1833

1934
TESTS_REQUIRES = [

heudiconv/tests/test_heuristics.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,13 @@ def test_notop(tmpdir, bidsoptions):
165165
runner(args)
166166

167167
assert op.exists(pjoin(tmppath, 'Halchenko/Yarik/950_bids_test4'))
168-
for fname in ['CHANGES', 'dataset_description.json', 'participants.tsv', 'README']:
168+
for fname in [
169+
'CHANGES',
170+
'dataset_description.json',
171+
'participants.tsv',
172+
'README',
173+
'participants.json'
174+
]:
169175
if 'notop' in bidsoptions:
170176
assert not op.exists(pjoin(tmppath, 'Halchenko/Yarik/950_bids_test4', fname))
171177
else:

heudiconv/tests/test_main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from os.path import join as opj
2323
from six.moves import StringIO
2424
import stat
25+
import os.path as op
2526

2627

2728
@patch('sys.stdout', new_callable=StringIO)
@@ -205,6 +206,7 @@ def _check_rows(fn, rows):
205206
assert dates == sorted(dates)
206207

207208
_check_rows(fn, rows)
209+
assert op.exists(opj(tmpdir.strpath, 'file.json'))
208210
# add a new one
209211
extra_rows = {
210212
'a_new_file.nii.gz': ['2016adsfasd23', '', 'fasadfasdf'],

heudiconv/tests/test_utils.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import json
12
import os
23
import os.path as op
4+
35
from heudiconv.utils import (
46
get_known_heuristics_with_descriptions,
57
get_heuristic_description,
68
load_heuristic,
7-
json_dumps_pretty)
9+
json_dumps_pretty,
10+
load_json,
11+
create_tree,
12+
save_json,
13+
JSONDecodeError)
814

915
import pytest
1016
from .utils import HEURISTICS_PATH
@@ -58,4 +64,24 @@ def test_json_dumps_pretty():
5864
'Mar 3 2017 10:46:13 by eja'
5965
# just the date which reveals the issue
6066
# tstr = 'Mar 3 2017 10:46:13 by eja'
61-
assert pretty({'WipMemBlock': tstr}) == '{\n "WipMemBlock": "%s"\n}' % tstr
67+
assert pretty({'WipMemBlock': tstr}) == '{\n "WipMemBlock": "%s"\n}' % tstr
68+
69+
70+
def test_load_json(tmp_path, caplog):
71+
# test invalid json
72+
ifname = 'invalid.json'
73+
invalid_json_file = str(tmp_path / ifname)
74+
create_tree(str(tmp_path), {ifname: u"I'm Jason Bourne"})
75+
76+
with pytest.raises(JSONDecodeError):
77+
load_json(str(invalid_json_file))
78+
79+
assert ifname in caplog.text
80+
81+
# test valid json
82+
vcontent = {"secret": "spy"}
83+
vfname = "valid.json"
84+
valid_json_file = str(tmp_path / vfname)
85+
save_json(valid_json_file, vcontent)
86+
87+
assert load_json(valid_json_file) == vcontent

heudiconv/utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@
1919
import logging
2020
lgr = logging.getLogger(__name__)
2121

22+
if sys.version_info[0] > 2:
23+
from json.decoder import JSONDecodeError
24+
else:
25+
JSONDecodeError = ValueError
26+
2227

2328
seqinfo_fields = [
2429
'total_files_till_now', # 0
@@ -172,10 +177,15 @@ def load_json(filename):
172177
-------
173178
data : dict
174179
"""
175-
with open(filename, 'r') as fp:
176-
data = json.load(fp)
177-
return data
180+
try:
181+
with open(filename, 'r') as fp:
182+
data = json.load(fp)
183+
except JSONDecodeError:
184+
lgr.error("{fname} is not a valid json file".format(fname=filename))
185+
raise
178186

187+
return data
188+
179189

180190
def assure_no_file_exists(path):
181191
"""Check if file or symlink (git-annex?) exists, and if so -- remove"""

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,13 @@ def findsome(subdir, extensions):
4747
description=ldict['__description__'],
4848
long_description=ldict['__longdesc__'],
4949
license=ldict['__license__'],
50+
classifiers=ldict['CLASSIFIERS'],
5051
packages=heudiconv_pkgs,
5152
entry_points={'console_scripts': [
5253
'heudiconv=heudiconv.cli.run:main',
5354
'heudiconv_monitor=heudiconv.cli.monitor:main',
5455
]},
56+
python_requires=ldict['PYTHON_REQUIRES'],
5557
install_requires=ldict['REQUIRES'],
5658
extras_require=ldict['EXTRA_REQUIRES'],
5759
package_data={

0 commit comments

Comments
 (0)