Skip to content

Commit 39348e8

Browse files
committed
Merge remote-tracking branch 'nipy/master'
2 parents 81367a0 + c15df99 commit 39348e8

File tree

5 files changed

+124
-20
lines changed

5 files changed

+124
-20
lines changed

.zenodo.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@
3838
"name": "Clark, Dav",
3939
"orcid": "0000-0002-3982-4416"
4040
},
41+
{
42+
"affiliation": "Shattuck Lab, UCLA Brain Mapping Center",
43+
"name": "Wong, Jason"
44+
},
4145
{
4246
"affiliation": "Mayo Clinic, Neurology, Rochester, MN, USA",
4347
"name": "Dayan, Michael",
@@ -302,8 +306,8 @@
302306
},
303307
{
304308
"affiliation": "MIT",
305-
"name": "Goncalves, Mathias",
306-
}
309+
"name": "Goncalves, Mathias"
310+
},
307311
{
308312
"affiliation": "MIT, HMS",
309313
"name": "Ghosh, Satrajit",

nipype/interfaces/utility/base.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,29 @@ def _list_outputs(self):
9999
class MergeInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec):
100100
axis = traits.Enum('vstack', 'hstack', usedefault=True,
101101
desc='direction in which to merge, hstack requires same number of elements in each input')
102-
no_flatten = traits.Bool(False, usedefault=True, desc='append to outlist instead of extending in vstack mode')
102+
no_flatten = traits.Bool(False, usedefault=True,
103+
desc='append to outlist instead of extending in vstack mode')
104+
ravel_inputs = traits.Bool(False, usedefault=True,
105+
desc='ravel inputs when no_flatten is False')
103106

104107

105108
class MergeOutputSpec(TraitedSpec):
106109
out = traits.List(desc='Merged output')
107110

108111

112+
def _ravel(in_val):
113+
if not isinstance(in_val, list):
114+
return in_val
115+
flat_list = []
116+
for val in in_val:
117+
raveled_val = _ravel(val)
118+
if isinstance(raveled_val, list):
119+
flat_list.extend(raveled_val)
120+
else:
121+
flat_list.append(raveled_val)
122+
return flat_list
123+
124+
109125
class Merge(IOBase):
110126
"""Basic interface class to merge inputs into a single list
111127
@@ -123,23 +139,34 @@ class Merge(IOBase):
123139
>>> out.outputs.out
124140
[1, 2, 5, 3]
125141
126-
>>> merge = Merge() # Or Merge(1)
127-
>>> merge.inputs.in_lists = [1, [2, 5], 3]
142+
>>> merge = Merge(1)
143+
>>> merge.inputs.in1 = [1, [2, 5], 3]
144+
>>> out = merge.run()
145+
>>> out.outputs.out
146+
[1, [2, 5], 3]
147+
148+
>>> merge = Merge(1)
149+
>>> merge.inputs.in1 = [1, [2, 5], 3]
150+
>>> merge.inputs.ravel_inputs = True
128151
>>> out = merge.run()
129152
>>> out.outputs.out
130153
[1, 2, 5, 3]
131154
155+
>>> merge = Merge(1)
156+
>>> merge.inputs.in1 = [1, [2, 5], 3]
157+
>>> merge.inputs.no_flatten = True
158+
>>> out = merge.run()
159+
>>> out.outputs.out
160+
[[1, [2, 5], 3]]
132161
"""
133162
input_spec = MergeInputSpec
134163
output_spec = MergeOutputSpec
135164

136-
def __init__(self, numinputs=1, **inputs):
165+
def __init__(self, numinputs=0, **inputs):
137166
super(Merge, self).__init__(**inputs)
138167
self._numinputs = numinputs
139-
if numinputs > 1:
168+
if numinputs >= 1:
140169
input_names = ['in%d' % (i + 1) for i in range(numinputs)]
141-
elif numinputs == 1:
142-
input_names = ['in_lists']
143170
else:
144171
input_names = []
145172
add_traits(self.inputs, input_names)
@@ -150,8 +177,6 @@ def _list_outputs(self):
150177

151178
if self._numinputs < 1:
152179
return outputs
153-
elif self._numinputs == 1:
154-
values = self.inputs.in_lists
155180
else:
156181
getval = lambda idx: getattr(self.inputs, 'in%d' % (idx + 1))
157182
values = [getval(idx) for idx in range(self._numinputs)
@@ -160,7 +185,8 @@ def _list_outputs(self):
160185
if self.inputs.axis == 'vstack':
161186
for value in values:
162187
if isinstance(value, list) and not self.inputs.no_flatten:
163-
out.extend(value)
188+
out.extend(_ravel(value) if self.inputs.ravel_inputs else
189+
value)
164190
else:
165191
out.append(value)
166192
else:

nipype/interfaces/utility/tests/test_base.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,20 @@ def test_split(tmpdir, args, expected):
5656
([3], {}, [0, [1, 2], [3, 4, 5]], [0, 1, 2, 3, 4, 5]),
5757
([0], {}, None, None),
5858
([], {}, [], []),
59-
([], {}, [0, [1, 2], [3, 4, 5]], [0, 1, 2, 3, 4, 5]),
59+
([], {}, [0, [1, 2], [3, 4, 5]], [0, [1, 2], [3, 4, 5]]),
6060
([3], {'axis': 'hstack'}, [[0], [1, 2], [3, 4, 5]], [[0, 1, 3]]),
6161
([3], {'axis': 'hstack'}, [[0, 1], [2, 3], [4, 5]],
6262
[[0, 2, 4], [1, 3, 5]]),
6363
([3], {'axis': 'hstack'}, [[0, 1], [2, 3], [4, 5]],
6464
[[0, 2, 4], [1, 3, 5]]),
65-
([1], {'axis': 'hstack'}, [[0], [1, 2], [3, 4, 5]], [[0, 1, 3]]),
66-
([1], {'axis': 'hstack'}, [[0, 1], [2, 3], [4, 5]],
67-
[[0, 2, 4], [1, 3, 5]]),
6865
])
6966
def test_merge(tmpdir, args, kwargs, in_lists, expected):
7067
os.chdir(str(tmpdir))
7168

7269
node = pe.Node(utility.Merge(*args, **kwargs), name='merge')
7370

74-
numinputs = args[0] if args else 1
75-
if numinputs == 1:
76-
node.inputs.in_lists = in_lists
77-
elif numinputs > 1:
71+
numinputs = args[0] if args else 0
72+
if numinputs >= 1:
7873
for i in range(1, numinputs + 1):
7974
setattr(node.inputs, 'in{:d}'.format(i), in_lists[i - 1])
8075

nipype/utils/filemanip.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import sys
1414
import pickle
15+
import subprocess
1516
import gzip
1617
import hashlib
1718
from hashlib import md5
@@ -237,6 +238,54 @@ def hash_timestamp(afile):
237238
return md5hex
238239

239240

241+
def _generate_cifs_table():
242+
"""Construct a reverse-length-ordered list of mount points that
243+
fall under a CIFS mount.
244+
245+
This precomputation allows efficient checking for whether a given path
246+
would be on a CIFS filesystem.
247+
248+
On systems without a ``mount`` command, or with no CIFS mounts, returns an
249+
empty list.
250+
"""
251+
exit_code, output = subprocess.getstatusoutput("mount")
252+
# Not POSIX
253+
if exit_code != 0:
254+
return []
255+
256+
# (path, fstype) tuples, sorted by path length (longest first)
257+
mount_info = sorted((line.split()[2:5:2] for line in output.splitlines()),
258+
key=lambda x: len(x[0]),
259+
reverse=True)
260+
cifs_paths = [path for path, fstype in mount_info if fstype == 'cifs']
261+
262+
return [mount for mount in mount_info
263+
if any(mount[0].startswith(path) for path in cifs_paths)]
264+
265+
266+
_cifs_table = _generate_cifs_table()
267+
268+
269+
def on_cifs(fname):
270+
""" Checks whether a file path is on a CIFS filesystem mounted in a POSIX
271+
host (i.e., has the ``mount`` command).
272+
273+
On Windows, Docker mounts host directories into containers through CIFS
274+
shares, which has support for Minshall+French symlinks, or text files that
275+
the CIFS driver exposes to the OS as symlinks.
276+
We have found that under concurrent access to the filesystem, this feature
277+
can result in failures to create or read recently-created symlinks,
278+
leading to inconsistent behavior and ``FileNotFoundError``s.
279+
280+
This check is written to support disabling symlinks on CIFS shares.
281+
"""
282+
# Only the first match (most recent parent) counts
283+
for fspath, fstype in _cifs_table:
284+
if fname.startswith(fspath):
285+
return fstype == 'cifs'
286+
return False
287+
288+
240289
def copyfile(originalfile, newfile, copy=False, create_new=False,
241290
hashmethod=None, use_hardlink=False,
242291
copy_related_files=True):
@@ -288,6 +337,10 @@ def copyfile(originalfile, newfile, copy=False, create_new=False,
288337
if hashmethod is None:
289338
hashmethod = config.get('execution', 'hash_method').lower()
290339

340+
# Don't try creating symlinks on CIFS
341+
if copy is False and on_cifs(newfile):
342+
copy = True
343+
291344
# Existing file
292345
# -------------
293346
# Options:

nipype/utils/tests/test_filemanip.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from ...utils.filemanip import (save_json, load_json,
1616
fname_presuffix, fnames_presuffix,
1717
hash_rename, check_forhash,
18+
_cifs_table, on_cifs,
1819
copyfile, copyfiles,
1920
filename_to_list, list_to_filename,
2021
check_depends,
@@ -334,3 +335,28 @@ def test_related_files(file, length, expected_files):
334335
for ef in expected_files:
335336
assert ef in related_files
336337

338+
339+
def test_cifs_check():
340+
assert isinstance(_cifs_table, list)
341+
assert isinstance(on_cifs('/'), bool)
342+
fake_table = [('/scratch/tmp', 'ext4'), ('/scratch', 'cifs')]
343+
cifs_targets = [('/scratch/tmp/x/y', False),
344+
('/scratch/tmp/x', False),
345+
('/scratch/x/y', True),
346+
('/scratch/x', True),
347+
('/x/y', False),
348+
('/x', False),
349+
('/', False)]
350+
351+
orig_table = _cifs_table[:]
352+
_cifs_table[:] = []
353+
354+
for target, _ in cifs_targets:
355+
assert on_cifs(target) is False
356+
357+
_cifs_table.extend(fake_table)
358+
for target, expected in cifs_targets:
359+
assert on_cifs(target) is expected
360+
361+
_cifs_table[:] = []
362+
_cifs_table.extend(orig_table)

0 commit comments

Comments
 (0)