Skip to content

Commit 8292a7a

Browse files
committed
enh: add resolving to the results loader and rebasing to saver
Fixes #2944.
1 parent 01a2772 commit 8292a7a

File tree

2 files changed

+111
-21
lines changed

2 files changed

+111
-21
lines changed

nipype/pipeline/engine/tests/test_utils.py

Lines changed: 59 additions & 0 deletions
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

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525
from ... import logging, config, LooseVersion
2626
from ...utils.filemanip import (
2727
Path,
28+
indirectory,
2829
relpath,
2930
makedirs,
3031
fname_presuffix,
3132
to_str,
3233
ensure_list,
3334
get_related_files,
34-
FileNotFoundError,
3535
save_json,
3636
savepkl,
3737
loadpkl,
@@ -41,6 +41,7 @@
4141
)
4242
from ...utils.misc import str2bool
4343
from ...utils.functions import create_function_from_source
44+
from ...interfaces.base.traits_extension import rebase_path_traits, resolve_path_traits
4445
from ...interfaces.base import (Bunch, CommandLine, isdefined, Undefined,
4546
InterfaceResult, traits)
4647
from ...interfaces.utility import IdentityInterface
@@ -227,52 +228,82 @@ def write_report(node, report_type=None, is_mapnode=False):
227228
return
228229

229230

230-
def save_resultfile(result, cwd, name):
231-
"""Save a result pklz file to ``cwd``"""
231+
def save_resultfile(result, cwd, name, rebase=True):
232+
"""Save a result pklz file to ``cwd``."""
233+
cwd = os.path.abspath(cwd)
232234
resultsfile = os.path.join(cwd, 'result_%s.pklz' % name)
233-
savepkl(resultsfile, result)
234-
logger.debug('saved results in %s', resultsfile)
235+
logger.debug("Saving results file: '%s'", resultsfile)
235236

237+
if result.outputs is None:
238+
logger.warn('Storing result file without outputs')
239+
savepkl(resultsfile, result)
240+
return
241+
try:
242+
outputs = result.outputs.trait_get()
243+
except AttributeError:
244+
logger.debug('Storing non-traited results, skipping rebase of paths')
245+
savepkl(resultsfile, result)
246+
return
236247

237-
def load_resultfile(results_file):
248+
try:
249+
with indirectory(cwd):
250+
# All the magic to fix #2944 resides here:
251+
for key, val in list(outputs.items()):
252+
val = rebase_path_traits(result.outputs.trait(key), val, cwd)
253+
setattr(result.outputs, key, val)
254+
savepkl(resultsfile, result)
255+
finally:
256+
# Reset resolved paths from the outputs dict no matter what
257+
for key, val in list(outputs.items()):
258+
setattr(result.outputs, key, val)
259+
260+
261+
def load_resultfile(results_file, resolve=True):
238262
"""
239-
Load InterfaceResult file from path
263+
Load InterfaceResult file from path.
240264
241265
Parameter
242266
---------
243-
244267
path : base_dir of node
245268
name : name of node
246269
247270
Returns
248271
-------
249-
250272
result : InterfaceResult structure
251273
aggregate : boolean indicating whether node should aggregate_outputs
252274
attribute error : boolean indicating whether there was some mismatch in
253275
versions of traits used to store result and hence node needs to
254276
rerun
277+
255278
"""
256-
aggregate = True
257279
results_file = Path(results_file)
280+
aggregate = True
258281
result = None
259282
attribute_error = False
260-
if results_file.exists():
283+
284+
if not results_file.exists():
285+
return result, aggregate, attribute_error
286+
287+
with indirectory(str(results_file.parent)):
261288
try:
262289
result = loadpkl(results_file)
263-
except (traits.TraitError, AttributeError, ImportError,
264-
EOFError) as err:
265-
if isinstance(err, (AttributeError, ImportError)):
266-
attribute_error = True
267-
logger.debug('attribute error: %s probably using '
268-
'different trait pickled file', str(err))
269-
else:
270-
logger.debug(
271-
'some file does not exist. hence trait cannot be set')
290+
except (traits.TraitError, EOFError):
291+
logger.debug(
292+
'some file does not exist. hence trait cannot be set')
293+
except (AttributeError, ImportError) as err:
294+
attribute_error = True
295+
logger.debug('attribute error: %s probably using '
296+
'different trait pickled file', str(err))
272297
else:
273298
aggregate = False
274299

275-
logger.debug('Aggregate: %s', aggregate)
300+
if resolve and not aggregate:
301+
logger.debug('Resolving paths in outputs loaded from results file.')
302+
for trait_name, old_value in list(result.outputs.get().items()):
303+
value = resolve_path_traits(result.outputs.trait(trait_name), old_value,
304+
results_file.parent)
305+
setattr(result.outputs, trait_name, value)
306+
276307
return result, aggregate, attribute_error
277308

278309

0 commit comments

Comments
 (0)