Repository: nipy/nipype

Issue:
With NiPype Memory with version 0.12.1 and >=0.13.0
Hi Nipype team,

### Summary
Trying to understand what is causing this difference between versions 0.12.1 and >=0.13.0. Is this with Memory or Realign. I am suspecting that this is the problem. But, I am sure I am missing some important details on this ?
 We are experiencing this in package pypreprocess which is reliant on Nipype. See full traceback in https://github.com/neurospin/pypreprocess/issues/288
### Actual behavior
```python
Nipype version:0.13.0
Traceback (most recent call last):
  File "report_nipype.py", line 11, in <module>
    print(realign)
  File "/home/kamalakar/miniconda2/envs/pypreprocess0_13/lib/python2.7/site-packages/nipype/caching/memory.py", line 98, in __repr__
    self.base_dir)
ValueError: Single '}' encountered in format string
```
### Expected behavior
```python
Nipype version:0.12.1
PipeFunc(nipype.interfaces.spm.preprocess.Realign, base_dir=/tmp/nipype_mem)
```
### How to replicate the behavior
```python
import nipype
print("Nipype version:{0}".format(nipype.__version__))

import nipype.interfaces.spm as spm
from nipype.caching import Memory

mem = Memory(base_dir='/tmp/')
realign = mem.cache(spm.Realign)
print(realign)
```
Thanks a lot for your time.


Comments:

Patch:
diff --git a/nipype/algorithms/icc.py b/nipype/algorithms/icc.py
index 133fa7974..5d5ec1c39 100644
--- a/nipype/algorithms/icc.py
+++ b/nipype/algorithms/icc.py
@@ -80,7 +80,6 @@ class ICC(BaseInterface):
     def _list_outputs(self):
         outputs = self._outputs().get()
         outputs['icc_map'] = os.path.abspath('icc_map.nii')
-        outputs['sessions_F_map'] = os.path.abspath('sessions_F_map.nii')
         outputs['session_var_map'] = os.path.abspath('session_var_map.nii')
         outputs['subject_var_map'] = os.path.abspath('subject_var_map.nii')
         return outputs
diff --git a/nipype/interfaces/afni/utils.py b/nipype/interfaces/afni/utils.py
index 188ba158c..8acdf29f9 100644
--- a/nipype/interfaces/afni/utils.py
+++ b/nipype/interfaces/afni/utils.py
@@ -1434,7 +1434,7 @@ class MergeInputSpec(AFNICommandInputSpec):
         name_template='%s_merge',
         desc='output image file name',
         argstr='-prefix %s',
-        name_source='in_file')
+        name_source='in_files')
     doall = traits.Bool(
         desc='apply options to all sub-bricks in dataset', argstr='-doall')
     blurfwhm = traits.Int(
@@ -2635,9 +2635,10 @@ class ZcatInputSpec(AFNICommandInputSpec):
         mandatory=True,
         copyfile=False)
     out_file = File(
-        name_template='zcat',
+        name_template='%s_zcat',
         desc='output dataset prefix name (default \'zcat\')',
-        argstr='-prefix %s')
+        argstr='-prefix %s',
+        name_source='in_files')
     datum = traits.Enum(
         'byte',
         'short',
diff --git a/nipype/interfaces/base/traits_extension.py b/nipype/interfaces/base/traits_extension.py
index 5b3e9f94d..6dfef8ebf 100644
--- a/nipype/interfaces/base/traits_extension.py
+++ b/nipype/interfaces/base/traits_extension.py
@@ -217,6 +217,16 @@ class ImageFile(File):
         super(ImageFile, self).__init__(value, filter, auto_set, entries,
                                         exists, **metadata)
 
+    def info(self):
+        existing = 'n existing' if self.exists else ''
+        comma = ',' if self.exists and not self.allow_compressed else ''
+        uncompressed = ' uncompressed' if not self.allow_compressed else ''
+        with_ext = ' (valid extensions: [{}])'.format(
+            ', '.join(self.grab_exts())) if self.types else ''
+        return 'a{existing}{comma}{uncompressed} file{with_ext}'.format(
+            existing=existing, comma=comma, uncompressed=uncompressed,
+            with_ext=with_ext)
+
     def grab_exts(self):
         # TODO: file type validation
         exts = []
@@ -243,11 +253,11 @@ class ImageFile(File):
         """
         validated_value = super(ImageFile, self).validate(object, name, value)
         if validated_value and self.types:
-            self._exts = self.grab_exts()
-            if not any(validated_value.endswith(x) for x in self._exts):
+            _exts = self.grab_exts()
+            if not any(validated_value.endswith(x) for x in _exts):
                 raise TraitError(
                     args="{} is not included in allowed types: {}".format(
-                        validated_value, ', '.join(self._exts)))
+                        validated_value, ', '.join(_exts)))
         return validated_value
 
 
@@ -322,15 +332,11 @@ class MultiPath(traits.List):
 
         newvalue = value
 
+        inner_trait = self.inner_traits()[0]
         if not isinstance(value, list) \
-            or (self.inner_traits() and
-                isinstance(self.inner_traits()[0].trait_type,
-                           traits.List) and not
-                isinstance(self.inner_traits()[0].trait_type,
-                           InputMultiPath) and
-                isinstance(value, list) and
-                value and not
-                isinstance(value[0], list)):
+            or (isinstance(inner_trait.trait_type, traits.List) and
+                not isinstance(inner_trait.trait_type, InputMultiPath) and
+                not isinstance(value[0], list)):
             newvalue = [value]
         value = super(MultiPath, self).validate(object, name, newvalue)
 
diff --git a/nipype/interfaces/spm/preprocess.py b/nipype/interfaces/spm/preprocess.py
index 47c9fd77a..c08cc4686 100644
--- a/nipype/interfaces/spm/preprocess.py
+++ b/nipype/interfaces/spm/preprocess.py
@@ -125,7 +125,8 @@ class SliceTiming(SPMCommand):
 
 class RealignInputSpec(SPMCommandInputSpec):
     in_files = InputMultiPath(
-        ImageFileSPM(exists=True),
+        traits.Either(ImageFileSPM(exists=True),
+                      traits.List(ImageFileSPM(exists=True))),
         field='data',
         mandatory=True,
         copyfile=True,
diff --git a/nipype/pipeline/plugins/base.py b/nipype/pipeline/plugins/base.py
index d7b7d6417..ff84937bc 100644
--- a/nipype/pipeline/plugins/base.py
+++ b/nipype/pipeline/plugins/base.py
@@ -7,13 +7,13 @@ from __future__ import (print_function, division, unicode_literals,
                         absolute_import)
 from builtins import range, object, open
 
+import sys
 from copy import deepcopy
 from glob import glob
 import os
 import shutil
-import sys
 from time import sleep, time
-from traceback import format_exc
+from traceback import format_exception
 
 import numpy as np
 import scipy.sparse as ssp
@@ -159,7 +159,7 @@ class DistributedPluginBase(PluginBase):
                             graph,
                             result={
                                 'result': None,
-                                'traceback': format_exc()
+                                'traceback': '\n'.join(format_exception(*sys.exc_info()))
                             }))
                 else:
                     if result:
@@ -244,7 +244,7 @@ class DistributedPluginBase(PluginBase):
         mapnodesubids = self.procs[jobid].get_subnodes()
         numnodes = len(mapnodesubids)
         logger.debug('Adding %d jobs for mapnode %s', numnodes,
-                     self.procs[jobid]._id)
+                     self.procs[jobid])
         for i in range(numnodes):
             self.mapnodesubids[self.depidx.shape[0] + i] = jobid
         self.procs.extend(mapnodesubids)
@@ -274,7 +274,7 @@ class DistributedPluginBase(PluginBase):
                 slots = None
             else:
                 slots = max(0, self.max_jobs - num_jobs)
-            logger.debug('Slots available: %s' % slots)
+            logger.debug('Slots available: %s', slots)
             if (num_jobs >= self.max_jobs) or (slots == 0):
                 break
 
@@ -303,14 +303,14 @@ class DistributedPluginBase(PluginBase):
                     self.proc_done[jobid] = True
                     self.proc_pending[jobid] = True
                     # Send job to task manager and add to pending tasks
-                    logger.info('Submitting: %s ID: %d' %
-                                (self.procs[jobid]._id, jobid))
+                    logger.info('Submitting: %s ID: %d',
+                                self.procs[jobid], jobid)
                     if self._status_callback:
                         self._status_callback(self.procs[jobid], 'start')
 
                     if not self._local_hash_check(jobid, graph):
                         if self.procs[jobid].run_without_submitting:
-                            logger.debug('Running node %s on master thread' %
+                            logger.debug('Running node %s on master thread',
                                          self.procs[jobid])
                             try:
                                 self.procs[jobid].run()
@@ -327,8 +327,8 @@ class DistributedPluginBase(PluginBase):
                                 self.proc_pending[jobid] = False
                             else:
                                 self.pending_tasks.insert(0, (tid, jobid))
-                    logger.info('Finished submitting: %s ID: %d' %
-                                (self.procs[jobid]._id, jobid))
+                    logger.info('Finished submitting: %s ID: %d',
+                                self.procs[jobid], jobid)
             else:
                 break
 
@@ -337,22 +337,38 @@ class DistributedPluginBase(PluginBase):
                 self.procs[jobid].config['execution']['local_hash_check']):
             return False
 
-        cached, updated = self.procs[jobid].is_cached()
+        try:
+            cached, updated = self.procs[jobid].is_cached()
+        except Exception:
+            logger.warning(
+                'Error while checking node hash, forcing re-run. '
+                'Although this error may not prevent the workflow from running, '
+                'it could indicate a major problem. Please report a new issue '
+                'at https://github.com/nipy/nipype/issues adding the following '
+                'information:\n\n\tNode: %s\n\tInterface: %s.%s\n\tTraceback:\n%s',
+                self.procs[jobid],
+                self.procs[jobid].interface.__module__,
+                self.procs[jobid].interface.__class__.__name__,
+                '\n'.join(format_exception(*sys.exc_info()))
+            )
+            return False
+
         logger.debug('Checking hash "%s" locally: cached=%s, updated=%s.',
-                    self.procs[jobid].fullname, cached, updated)
+                     self.procs[jobid], cached, updated)
         overwrite = self.procs[jobid].overwrite
-        always_run = self.procs[jobid]._interface.always_run
+        always_run = self.procs[jobid].interface.always_run
 
         if cached and updated and (overwrite is False or
                                    overwrite is None and not always_run):
             logger.debug('Skipping cached node %s with ID %s.',
-                         self.procs[jobid]._id, jobid)
+                         self.procs[jobid], jobid)
             try:
                 self._task_finished_cb(jobid, cached=True)
                 self._remove_node_dirs()
             except Exception:
-                logger.debug('Error skipping cached node %s (%s).',
-                             self.procs[jobid]._id, jobid)
+                logger.debug('Error skipping cached node %s (%s).\n\n%s',
+                             self.procs[jobid], jobid,
+                             '\n'.join(format_exception(*sys.exc_info())))
                 self._clean_queue(jobid, graph)
                 self.proc_pending[jobid] = False
             return True
@@ -364,7 +380,7 @@ class DistributedPluginBase(PluginBase):
         This is called when a job is completed.
         """
         logger.info('[Job %d] %s (%s).', jobid, 'Cached'
-                    if cached else 'Completed', self.procs[jobid].fullname)
+                    if cached else 'Completed', self.procs[jobid])
         if self._status_callback:
             self._status_callback(self.procs[jobid], 'end')
         # Update job and worker queues
@@ -481,7 +497,7 @@ class SGELikeBatchManagerBase(DistributedPluginBase):
                                      taskid, timeout, node_dir))
                 raise IOError(error_message)
             except IOError as e:
-                result_data['traceback'] = format_exc()
+                result_data['traceback'] = '\n'.join(format_exception(*sys.exc_info()))
         else:
             results_file = glob(os.path.join(node_dir, 'result_*.pklz'))[0]
             result_data = loadpkl(results_file)


Test patch:
diff --git a/nipype/interfaces/afni/tests/test_auto_Merge.py b/nipype/interfaces/afni/tests/test_auto_Merge.py
index 02c8f9ca2..7136d04df 100644
--- a/nipype/interfaces/afni/tests/test_auto_Merge.py
+++ b/nipype/interfaces/afni/tests/test_auto_Merge.py
@@ -32,7 +32,7 @@ def test_Merge_inputs():
         ),
         out_file=dict(
             argstr='-prefix %s',
-            name_source='in_file',
+            name_source='in_files',
             name_template='%s_merge',
         ),
         outputtype=dict(),
diff --git a/nipype/interfaces/afni/tests/test_auto_Zcat.py b/nipype/interfaces/afni/tests/test_auto_Zcat.py
index d15deb91f..4a631c6ad 100644
--- a/nipype/interfaces/afni/tests/test_auto_Zcat.py
+++ b/nipype/interfaces/afni/tests/test_auto_Zcat.py
@@ -36,7 +36,8 @@ def test_Zcat_inputs():
         ),
         out_file=dict(
             argstr='-prefix %s',
-            name_template='zcat',
+            name_source='in_files',
+            name_template='%s_zcat',
         ),
         outputtype=dict(),
         terminal_output=dict(