Skip to content

Commit 72e2e96

Browse files
committed
enh: add resolving to the results loader and rebasing to saver
Fixes #2944.
1 parent b819505 commit 72e2e96

File tree

2 files changed

+118
-55
lines changed

2 files changed

+118
-55
lines changed

nipype/pipeline/engine/tests/test_utils.py

+59
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,62 @@ def test_mapnode_crash3(tmpdir):
224224
wf.config["execution"]["crashdump_dir"] = os.getcwd()
225225
with pytest.raises(RuntimeError):
226226
wf.run(plugin='Linear')
227+
228+
class StrPathConfuserInputSpec(nib.TraitedSpec):
229+
in_str = nib.traits.String()
230+
231+
232+
class StrPathConfuserOutputSpec(nib.TraitedSpec):
233+
out_tuple = nib.traits.Tuple(nib.File, nib.traits.String)
234+
out_dict_path = nib.traits.Dict(nib.traits.String, nib.File(exists=True))
235+
out_dict_str = nib.traits.DictStrStr()
236+
out_list = nib.traits.List(nib.traits.String)
237+
out_str = nib.traits.String()
238+
out_path = nib.File(exists=True)
239+
240+
241+
class StrPathConfuser(nib.SimpleInterface):
242+
input_spec = StrPathConfuserInputSpec
243+
output_spec = StrPathConfuserOutputSpec
244+
245+
def _run_interface(self, runtime):
246+
out_path = os.path.abspath(os.path.basename(self.inputs.in_str) + '_path')
247+
open(out_path, 'w').close()
248+
self._results['out_str'] = self.inputs.in_str
249+
self._results['out_path'] = out_path
250+
self._results['out_tuple'] = (out_path, self.inputs.in_str)
251+
self._results['out_dict_path'] = {self.inputs.in_str: out_path}
252+
self._results['out_dict_str'] = {self.inputs.in_str: self.inputs.in_str}
253+
self._results['out_list'] = [self.inputs.in_str] * 2
254+
return runtime
255+
256+
257+
def test_modify_paths_bug(tmpdir):
258+
"""
259+
There was a bug in which, if the current working directory contained a file with the name
260+
of an output String, the string would get transformed into a path, and generally wreak havoc.
261+
This attempts to replicate that condition, using an object with strings and paths in various
262+
trait configurations, to ensure that the guards added resolve the issue.
263+
Please see https://github.com/nipy/nipype/issues/2944 for more details.
264+
"""
265+
tmpdir.chdir()
266+
267+
spc = pe.Node(StrPathConfuser(in_str='2'), name='spc')
268+
269+
open('2', 'w').close()
270+
271+
outputs = spc.run().outputs
272+
273+
# Basic check that string was not manipulated
274+
out_str = outputs.out_str
275+
assert out_str == '2'
276+
277+
# Check path exists and is absolute
278+
out_path = outputs.out_path
279+
assert os.path.isabs(out_path)
280+
281+
# Assert data structures pass through correctly
282+
assert outputs.out_tuple == (out_path, out_str)
283+
assert outputs.out_dict_path == {out_str: out_path}
284+
assert outputs.out_dict_str == {out_str: out_str}
285+
assert outputs.out_list == [out_str] * 2

nipype/pipeline/engine/utils.py

+59-55
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from ... import logging, config, LooseVersion
2727
from ...utils.filemanip import (
28+
indirectory,
2829
relpath,
2930
makedirs,
3031
fname_presuffix,
@@ -40,6 +41,7 @@
4041
)
4142
from ...utils.misc import str2bool
4243
from ...utils.functions import create_function_from_source
44+
from ...interfaces.base.traits_extension import rebase_path_traits, resolve_path_traits
4345
from ...interfaces.base import (Bunch, CommandLine, isdefined, Undefined,
4446
InterfaceResult, traits)
4547
from ...interfaces.utility import IdentityInterface
@@ -284,86 +286,88 @@ def _protect_collapses(hastraits):
284286
return _uncollapse(hastraits.trait_get(), collapsed)
285287

286288

287-
def save_resultfile(result, cwd, name):
288-
"""Save a result pklz file to ``cwd``"""
289+
def save_resultfile(result, cwd, name, rebase=True):
290+
"""Save a result pklz file to ``cwd``."""
291+
cwd = os.path.abspath(cwd)
289292
resultsfile = os.path.join(cwd, 'result_%s.pklz' % name)
290-
if result.outputs:
291-
try:
292-
collapsed = _identify_collapses(result.outputs)
293-
outputs = _uncollapse(result.outputs.trait_get(), collapsed)
294-
# Double-protect tosave so that the original, uncollapsed trait
295-
# is saved in the pickle file. Thus, when the loading process
296-
# collapses, the original correct value is loaded.
297-
tosave = _uncollapse(outputs.copy(), collapsed)
298-
except AttributeError:
299-
tosave = outputs = result.outputs.dictcopy() # outputs was a bunch
300-
for k, v in list(modify_paths(tosave, relative=True, basedir=cwd).items()):
301-
setattr(result.outputs, k, v)
302-
303-
savepkl(resultsfile, result)
304-
logger.debug('saved results in %s', resultsfile)
305-
306-
if result.outputs:
307-
for k, v in list(outputs.items()):
308-
setattr(result.outputs, k, v)
309-
310-
311-
def load_resultfile(path, name):
293+
logger.debug("Saving results file: '%s'", resultsfile)
294+
295+
if result.outputs is None:
296+
logger.warn('Storing result file without outputs')
297+
savepkl(resultsfile, result)
298+
return
299+
300+
try:
301+
outputs = result.outputs.trait_get()
302+
except AttributeError:
303+
logger.debug('Storing non-traited results, skipping rebase of paths')
304+
savepkl(resultsfile, result)
305+
return
306+
307+
try:
308+
with indirectory(cwd):
309+
# All the magic to fix #2944 resides here:
310+
for key, val in list(outputs.items()):
311+
val = rebase_path_traits(result.outputs.trait(key), val, cwd)
312+
setattr(result.outputs, key, val)
313+
savepkl(resultsfile, result)
314+
finally:
315+
# Reset resolved paths from the outputs dict no matter what
316+
for key, val in list(outputs.items()):
317+
setattr(result.outputs, key, val)
318+
319+
320+
def load_resultfile(path, name, resolve=True):
312321
"""
313-
Load InterfaceResult file from path
322+
Load InterfaceResult file from path.
314323
315324
Parameter
316325
---------
317-
318326
path : base_dir of node
319327
name : name of node
320328
321329
Returns
322330
-------
323-
324331
result : InterfaceResult structure
325332
aggregate : boolean indicating whether node should aggregate_outputs
326333
attribute error : boolean indicating whether there was some mismatch in
327334
versions of traits used to store result and hence node needs to
328335
rerun
336+
329337
"""
330338
aggregate = True
331-
resultsoutputfile = os.path.join(path, 'result_%s.pklz' % name)
332339
result = None
333340
attribute_error = False
334-
if os.path.exists(resultsoutputfile):
335-
pkl_file = gzip.open(resultsoutputfile, 'rb')
341+
resultsoutputfile = os.path.join(path, 'result_%s.pklz' % name)
342+
if not os.path.exists(resultsoutputfile):
343+
return result, aggregate, attribute_error
344+
345+
with indirectory(path):
346+
pkl_file = gzip.open('result_%s.pklz' % name, 'rb')
336347
try:
337348
result = pickle.load(pkl_file)
338349
except UnicodeDecodeError:
339350
# Was this pickle created with Python 2.x?
340-
pickle.load(pkl_file, fix_imports=True, encoding='utf-8')
341-
logger.warning('Successfully loaded pkl in compatibility mode')
342-
except (traits.TraitError, AttributeError, ImportError,
343-
EOFError) as err:
344-
if isinstance(err, (AttributeError, ImportError)):
345-
attribute_error = True
346-
logger.debug('attribute error: %s probably using '
347-
'different trait pickled file', str(err))
348-
else:
349-
logger.debug(
350-
'some file does not exist. hence trait cannot be set')
351+
result = pickle.load(pkl_file, fix_imports=True, encoding='utf-8')
352+
logger.warning('Successfully loaded pkl in compatibility mode.')
353+
except (traits.TraitError, EOFError):
354+
logger.debug(
355+
'some file does not exist. hence trait cannot be set')
356+
except (AttributeError, ImportError) as err:
357+
attribute_error = True
358+
logger.debug('attribute error: %s probably using '
359+
'different trait pickled file', str(err))
351360
else:
352-
if result.outputs:
353-
try:
354-
outputs = _protect_collapses(result.outputs)
355-
except AttributeError:
356-
outputs = result.outputs.dictcopy() # outputs == Bunch
357-
try:
358-
for k, v in list(modify_paths(outputs, relative=False,
359-
basedir=path).items()):
360-
setattr(result.outputs, k, v)
361-
except FileNotFoundError:
362-
logger.debug('conversion to full path results in '
363-
'non existent file')
364361
aggregate = False
365-
pkl_file.close()
366-
logger.debug('Aggregate: %s', aggregate)
362+
finally:
363+
pkl_file.close()
364+
365+
if resolve and not aggregate:
366+
logger.debug('Resolving paths in outputs loaded from results file.')
367+
for trait_name, old_value in list(result.outputs.get().items()):
368+
value = resolve_path_traits(result.outputs.trait(trait_name), old_value, path)
369+
setattr(result.outputs, trait_name, value)
370+
367371
return result, aggregate, attribute_error
368372

369373

0 commit comments

Comments
 (0)