Skip to content

Commit 01de656

Browse files
authored
Merge pull request #3023 from oesteban/fix/revise-load_resultfile
FIX: Disallow returning ``None`` in ``pipeline.utils.load_resultfile``
2 parents 2162c84 + a200bc5 commit 01de656

File tree

5 files changed

+143
-84
lines changed

5 files changed

+143
-84
lines changed

nipype/pipeline/engine/nodes.py

+37-29
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def interface(self):
195195
def result(self):
196196
"""Get result from result file (do not hold it in memory)"""
197197
return _load_resultfile(
198-
op.join(self.output_dir(), 'result_%s.pklz' % self.name))[0]
198+
op.join(self.output_dir(), 'result_%s.pklz' % self.name))
199199

200200
@property
201201
def inputs(self):
@@ -518,7 +518,7 @@ def _get_inputs(self):
518518
logger.debug('input: %s', key)
519519
results_file = info[0]
520520
logger.debug('results file: %s', results_file)
521-
outputs = _load_resultfile(results_file)[0].outputs
521+
outputs = _load_resultfile(results_file).outputs
522522
if outputs is None:
523523
raise RuntimeError("""\
524524
Error populating the input "%s" of node "%s": the results file of the source node \
@@ -565,34 +565,42 @@ def _run_interface(self, execute=True, updatehash=False):
565565

566566
def _load_results(self):
567567
cwd = self.output_dir()
568-
result, aggregate, attribute_error = _load_resultfile(
569-
op.join(cwd, 'result_%s.pklz' % self.name))
568+
569+
try:
570+
result = _load_resultfile(
571+
op.join(cwd, 'result_%s.pklz' % self.name))
572+
except (traits.TraitError, EOFError):
573+
logger.debug(
574+
'Error populating inputs/outputs, (re)aggregating results...')
575+
except (AttributeError, ImportError) as err:
576+
logger.debug('attribute error: %s probably using '
577+
'different trait pickled file', str(err))
578+
old_inputs = loadpkl(op.join(cwd, '_inputs.pklz'))
579+
self.inputs.trait_set(**old_inputs)
580+
else:
581+
return result
582+
570583
# try aggregating first
571-
if aggregate:
572-
logger.debug('aggregating results')
573-
if attribute_error:
574-
old_inputs = loadpkl(op.join(cwd, '_inputs.pklz'))
575-
self.inputs.trait_set(**old_inputs)
576-
if not isinstance(self, MapNode):
577-
self._copyfiles_to_wd(linksonly=True)
578-
aggouts = self._interface.aggregate_outputs(
579-
needed_outputs=self.needed_outputs)
580-
runtime = Bunch(
581-
cwd=cwd,
582-
returncode=0,
583-
environ=dict(os.environ),
584-
hostname=socket.gethostname())
585-
result = InterfaceResult(
586-
interface=self._interface.__class__,
587-
runtime=runtime,
588-
inputs=self._interface.inputs.get_traitsfree(),
589-
outputs=aggouts)
590-
_save_resultfile(
591-
result, cwd, self.name,
592-
rebase=str2bool(self.config['execution']['use_relative_paths']))
593-
else:
594-
logger.debug('aggregating mapnode results')
595-
result = self._run_interface()
584+
if not isinstance(self, MapNode):
585+
self._copyfiles_to_wd(linksonly=True)
586+
aggouts = self._interface.aggregate_outputs(
587+
needed_outputs=self.needed_outputs)
588+
runtime = Bunch(
589+
cwd=cwd,
590+
returncode=0,
591+
environ=dict(os.environ),
592+
hostname=socket.gethostname())
593+
result = InterfaceResult(
594+
interface=self._interface.__class__,
595+
runtime=runtime,
596+
inputs=self._interface.inputs.get_traitsfree(),
597+
outputs=aggouts)
598+
_save_resultfile(
599+
result, cwd, self.name,
600+
rebase=str2bool(self.config['execution']['use_relative_paths']))
601+
else:
602+
logger.debug('aggregating mapnode results')
603+
result = self._run_interface()
596604
return result
597605

598606
def _run_command(self, execute, copyfiles=True):

nipype/pipeline/engine/tests/test_utils.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from ....interfaces import base as nib
1717
from ....interfaces import utility as niu
1818
from .... import config
19-
from ..utils import clean_working_directory, write_workflow_prov
19+
from ..utils import (clean_working_directory, write_workflow_prov,
20+
load_resultfile)
2021

2122

2223
class InputSpec(nib.TraitedSpec):
@@ -283,3 +284,49 @@ def test_modify_paths_bug(tmpdir):
283284
assert outputs.out_dict_path == {out_str: out_path}
284285
assert outputs.out_dict_str == {out_str: out_str}
285286
assert outputs.out_list == [out_str] * 2
287+
288+
289+
@pytest.mark.xfail(sys.version_info < (3, 4),
290+
reason="rebase does not fully work with Python 2.7")
291+
@pytest.mark.parametrize("use_relative", [True, False])
292+
def test_save_load_resultfile(tmpdir, use_relative):
293+
"""Test minimally the save/load functions for result files."""
294+
from shutil import copytree, rmtree
295+
tmpdir.chdir()
296+
297+
old_use_relative = config.getboolean('execution', 'use_relative_paths')
298+
config.set('execution', 'use_relative_paths', use_relative)
299+
300+
spc = pe.Node(StrPathConfuser(in_str='2'), name='spc')
301+
spc.base_dir = tmpdir.mkdir('node').strpath
302+
303+
result = spc.run()
304+
305+
loaded_result = load_resultfile(
306+
tmpdir.join('node').join('spc').join('result_spc.pklz').strpath)
307+
308+
assert result.runtime.dictcopy() == loaded_result.runtime.dictcopy()
309+
assert result.inputs == loaded_result.inputs
310+
assert result.outputs.get() == loaded_result.outputs.get()
311+
312+
# Test the mobility of the result file.
313+
copytree(tmpdir.join('node').strpath, tmpdir.join('node2').strpath)
314+
rmtree(tmpdir.join('node').strpath)
315+
316+
if use_relative:
317+
loaded_result2 = load_resultfile(
318+
tmpdir.join('node2').join('spc').join('result_spc.pklz').strpath)
319+
320+
assert result.runtime.dictcopy() == loaded_result2.runtime.dictcopy()
321+
assert result.inputs == loaded_result2.inputs
322+
assert loaded_result2.outputs.get() != result.outputs.get()
323+
newpath = result.outputs.out_path.replace('/node/', '/node2/')
324+
assert loaded_result2.outputs.out_path == newpath
325+
assert loaded_result2.outputs.out_tuple[0] == newpath
326+
assert loaded_result2.outputs.out_dict_path['2'] == newpath
327+
else:
328+
with pytest.raises(nib.TraitError):
329+
load_resultfile(
330+
tmpdir.join('node2').join('spc').join('result_spc.pklz').strpath)
331+
332+
config.set('execution', 'use_relative_paths', old_use_relative)

nipype/pipeline/engine/utils.py

+35-44
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,12 @@
3838
write_rst_header,
3939
write_rst_dict,
4040
write_rst_list,
41+
FileNotFoundError,
4142
)
4243
from ...utils.misc import str2bool
4344
from ...utils.functions import create_function_from_source
4445
from ...interfaces.base.traits_extension import (
45-
rebase_path_traits, resolve_path_traits, OutputMultiPath, isdefined, Undefined, traits)
46+
rebase_path_traits, resolve_path_traits, OutputMultiPath, isdefined, Undefined)
4647
from ...interfaces.base.support import Bunch, InterfaceResult
4748
from ...interfaces.base import CommandLine
4849
from ...interfaces.utility import IdentityInterface
@@ -249,6 +250,10 @@ def save_resultfile(result, cwd, name, rebase=None):
249250
savepkl(resultsfile, result)
250251
return
251252

253+
if not rebase:
254+
savepkl(resultsfile, result)
255+
return
256+
252257
backup_traits = {}
253258
try:
254259
with indirectory(cwd):
@@ -273,58 +278,44 @@ def load_resultfile(results_file, resolve=True):
273278
"""
274279
Load InterfaceResult file from path.
275280
276-
Parameter
277-
---------
278-
path : base_dir of node
279-
name : name of node
281+
Parameters
282+
----------
283+
results_file : pathlike
284+
Path to an existing pickle (``result_<interface name>.pklz``) created with
285+
``save_resultfile``.
286+
Raises ``FileNotFoundError`` if ``results_file`` does not exist.
287+
resolve : bool
288+
Determines whether relative paths will be resolved to absolute (default is ``True``).
280289
281290
Returns
282291
-------
283-
result : InterfaceResult structure
284-
aggregate : boolean indicating whether node should aggregate_outputs
285-
attribute error : boolean indicating whether there was some mismatch in
286-
versions of traits used to store result and hence node needs to
287-
rerun
292+
result : InterfaceResult
293+
A Nipype object containing the runtime, inputs, outputs and other interface information
294+
such as a traceback in the case of errors.
288295
289296
"""
290297
results_file = Path(results_file)
291-
aggregate = True
292-
result = None
293-
attribute_error = False
294-
295298
if not results_file.exists():
296-
return result, aggregate, attribute_error
299+
raise FileNotFoundError(results_file)
297300

298-
with indirectory(str(results_file.parent)):
301+
result = loadpkl(results_file)
302+
if resolve and result.outputs:
299303
try:
300-
result = loadpkl(results_file)
301-
except (traits.TraitError, EOFError):
302-
logger.debug(
303-
'some file does not exist. hence trait cannot be set')
304-
except (AttributeError, ImportError) as err:
305-
attribute_error = True
306-
logger.debug('attribute error: %s probably using '
307-
'different trait pickled file', str(err))
308-
else:
309-
aggregate = False
310-
311-
if resolve and result.outputs:
312-
try:
313-
outputs = result.outputs.get()
314-
except TypeError: # This is a Bunch
315-
return result, aggregate, attribute_error
316-
317-
logger.debug('Resolving paths in outputs loaded from results file.')
318-
for trait_name, old in list(outputs.items()):
319-
if isdefined(old):
320-
if result.outputs.trait(trait_name).is_trait_type(OutputMultiPath):
321-
old = result.outputs.trait(trait_name).handler.get_value(
322-
result.outputs, trait_name)
323-
value = resolve_path_traits(result.outputs.trait(trait_name), old,
324-
results_file.parent)
325-
setattr(result.outputs, trait_name, value)
326-
327-
return result, aggregate, attribute_error
304+
outputs = result.outputs.get()
305+
except TypeError: # This is a Bunch
306+
logger.debug('Outputs object of loaded result %s is a Bunch.', results_file)
307+
return result
308+
309+
logger.debug('Resolving paths in outputs loaded from results file.')
310+
for trait_name, old in list(outputs.items()):
311+
if isdefined(old):
312+
if result.outputs.trait(trait_name).is_trait_type(OutputMultiPath):
313+
old = result.outputs.trait(trait_name).handler.get_value(
314+
result.outputs, trait_name)
315+
value = resolve_path_traits(result.outputs.trait(trait_name), old,
316+
results_file.parent)
317+
setattr(result.outputs, trait_name, value)
318+
return result
328319

329320

330321
def strip_temp(files, wd):

nipype/pipeline/plugins/tools.py

+22-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from traceback import format_exception
1717

1818
from ... import logging
19-
from ...utils.filemanip import savepkl, crash2txt, makedirs
19+
from ...utils.filemanip import savepkl, crash2txt, makedirs, FileNotFoundError
2020

2121
logger = logging.getLogger('nipype.workflow')
2222

@@ -26,17 +26,30 @@ def report_crash(node, traceback=None, hostname=None):
2626
"""
2727
name = node._id
2828
host = None
29-
if node.result and getattr(node.result, 'runtime'):
30-
if isinstance(node.result.runtime, list):
31-
host = node.result.runtime[0].hostname
32-
else:
33-
host = node.result.runtime.hostname
29+
traceback = traceback or format_exception(*sys.exc_info())
30+
31+
try:
32+
result = node.result
33+
except FileNotFoundError:
34+
traceback += """
35+
36+
When creating this crashfile, the results file corresponding
37+
to the node could not be found.""".splitlines(keepends=True)
38+
except Exception as exc:
39+
traceback += """
40+
41+
During the creation of this crashfile triggered by the above exception,
42+
another exception occurred:\n\n{}.""".format(exc).splitlines(keepends=True)
43+
else:
44+
if getattr(result, 'runtime', None):
45+
if isinstance(result.runtime, list):
46+
host = result.runtime[0].hostname
47+
else:
48+
host = result.runtime.hostname
3449

3550
# Try everything to fill in the host
3651
host = host or hostname or gethostname()
3752
logger.error('Node %s failed to run on host %s.', name, host)
38-
if not traceback:
39-
traceback = format_exception(*sys.exc_info())
4053
timeofcrash = strftime('%Y%m%d-%H%M%S')
4154
try:
4255
login_name = getpass.getuser()
@@ -49,7 +62,7 @@ def report_crash(node, traceback=None, hostname=None):
4962
makedirs(crashdir, exist_ok=True)
5063
crashfile = os.path.join(crashdir, crashfile)
5164

52-
if node.config['execution']['crashfile_format'].lower() in ['text', 'txt']:
65+
if node.config['execution']['crashfile_format'].lower() in ('text', 'txt', '.txt'):
5366
crashfile += '.txt'
5467
else:
5568
crashfile += '.pklz'

nipype/utils/filemanip.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ def loadpkl(infile):
708708
Attempted to open a results file generated by Nipype version %s, \
709709
with an incompatible Nipype version (%s)""", pkl_metadata['version'], version)
710710
raise e
711-
fmlogger.error("""\
711+
fmlogger.warning("""\
712712
No metadata was found in the pkl file. Make sure you are currently using \
713713
the same Nipype version from the generated pkl.""")
714714
raise e

0 commit comments

Comments
 (0)