Repository: nipy/nipype

Issue:
Fix name_source to handle missing source
As discussed in #2450, the `name_source` logic is not working as expected.

Consider the following input spec:

```Python
class NwarpAdjustInputSpec(AFNICommandInputSpec):
    warps = InputMultiPath(
        File(exists=True),
        minlen=5,
        mandatory=True,
        argstr='-nwarp %s',
        desc='List of input 3D warp datasets')
    in_files = InputMultiPath(
        File(exists=True),
        minlen=5,
        argstr='-source %s',
        desc='List of input 3D datasets to be warped by the adjusted warp '
             'datasets.  There must be exactly as many of these datasets as '
             'there are input warps.')
    out_file = File(
        desc='Output mean dataset, only needed if in_files are also given. '
             'The output dataset will be on the common grid shared by the '
             'source datasets.',
        argstr='-prefix %s',
        name_source='in_files',
        name_template='%s_NwarpAdjust',
        keep_extension=True,
        requires=['in_files'])
```

The goal is to automatically derive an `out_file` value iff `in_files` is set. There was some discussion over whether the traits are correct as is (e.g. @oesteban suggested that `requires` is not needed for `out_file`), but it did seem to be agreed that the goal should be achievable purely using traits without overriding interface methods.


Discussion:
Figure I should bump this before we get too close to release. Does anybody have time to figure this out?

@oesteban @satra Are there open issues to discuss, or is it a question of implementation?
I think it is mostly a question of implementation

Resolving patch:
diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py
--- a/nipype/interfaces/base/core.py
+++ b/nipype/interfaces/base/core.py
@@ -1074,8 +1074,13 @@ def _filename_from_source(self, name, chain=None):
                 return retval
 
             # Do not generate filename when excluded by other inputs
-            if trait_spec.xor and any(isdefined(getattr(self.inputs, field))
-                                      for field in trait_spec.xor):
+            if any(isdefined(getattr(self.inputs, field))
+                   for field in trait_spec.xor or ()):
+                return retval
+
+            # Do not generate filename when required fields are missing
+            if not all(isdefined(getattr(self.inputs, field))
+                       for field in trait_spec.requires or ()):
                 return retval
 
             if isdefined(retval) and "%s" in retval:
@@ -1118,6 +1123,9 @@ def _filename_from_source(self, name, chain=None):
                 base = self._filename_from_source(ns, chain)
                 if isdefined(base):
                     _, _, source_ext = split_filename(base)
+                else:
+                    # Do not generate filename when required fields are missing
+                    return retval
 
             chain = None
             retval = name_template % base
@@ -1144,8 +1152,9 @@ def _list_outputs(self):
                 out_name = name
                 if trait_spec.output_name is not None:
                     out_name = trait_spec.output_name
-                outputs[out_name] = \
-                    os.path.abspath(self._filename_from_source(name))
+                fname = self._filename_from_source(name)
+                if isdefined(fname):
+                    outputs[out_name] = os.path.abspath(fname)
             return outputs
 
     def _parse_inputs(self, skip=None):
diff --git a/nipype/interfaces/niftyfit/dwi.py b/nipype/interfaces/niftyfit/dwi.py
--- a/nipype/interfaces/niftyfit/dwi.py
+++ b/nipype/interfaces/niftyfit/dwi.py
@@ -66,13 +66,11 @@ class FitDwiInputSpec(CommandLineInputSpec):
         argstr='-nodiff %s')
 
     # Output options, with templated output names based on the source image
-    desc = 'Filename of multi-compartment model parameter map \
-(-ivim,-ball,-nod)'
-
     mcmap_file = traits.File(
         name_source=['source_file'],
         name_template='%s_mcmap.nii.gz',
-        desc=desc,
+        desc='Filename of multi-compartment model parameter map '
+             '(-ivim,-ball,-nod)',
         argstr='-mcmap %s',
         requires=['nodv_flag'])
 
@@ -239,10 +237,7 @@ class FitDwiInputSpec(CommandLineInputSpec):
 
 class FitDwiOutputSpec(TraitedSpec):
     """ Output Spec for FitDwi. """
-    desc = 'Filename of multi-compartment model parameter map \
-(-ivim,-ball,-nod)'
 
-    mcmap_file = traits.File(desc=desc)
     error_file = traits.File(desc='Filename of parameter error maps')
     res_file = traits.File(desc='Filename of model residual map')
     syn_file = traits.File(desc='Filename of synthetic image')
@@ -253,10 +248,9 @@ class FitDwiOutputSpec(TraitedSpec):
     rgbmap_file = traits.File(desc='Filename of colour FA map')
     tenmap_file = traits.File(desc='Filename of tensor map')
     tenmap2_file = traits.File(desc='Filename of tensor map [lower tri]')
-    desc = 'Filename of multi-compartment model parameter map \
-(-ivim,-ball,-nod).'
 
-    mcmap_file = traits.File(desc=desc)
+    mcmap_file = traits.File(desc='Filename of multi-compartment model '
+                                  'parameter map (-ivim,-ball,-nod).')
     mcout = traits.File(desc='Filename of mc samples (ascii text file)')
 
 
@@ -281,10 +275,10 @@ class FitDwi(NiftyFitCommand):
     >>> fit_dwi.inputs.rgbmap_file = 'rgb.nii.gz'
     >>> fit_dwi.cmdline
     'fit_dwi -source dwi.nii.gz -bval bvals -bvec bvecs -dti \
--error dwi_error.nii.gz -famap dwi_famap.nii.gz -mcmap dwi_mcmap.nii.gz \
--mcout dwi_mcout.txt -mdmap dwi_mdmap.nii.gz -nodiff dwi_no_diff.nii.gz \
--res dwi_resmap.nii.gz -rgbmap rgb.nii.gz -syn dwi_syn.nii.gz \
--tenmap2 dwi_tenmap2.nii.gz  -v1map dwi_v1map.nii.gz'
+-error dwi_error.nii.gz -famap dwi_famap.nii.gz -mcout dwi_mcout.txt \
+-mdmap dwi_mdmap.nii.gz -nodiff dwi_no_diff.nii.gz -res dwi_resmap.nii.gz \
+-rgbmap rgb.nii.gz -syn dwi_syn.nii.gz -tenmap2 dwi_tenmap2.nii.gz \
+-v1map dwi_v1map.nii.gz'
 
     """
     _cmd = get_custom_path('fit_dwi', env_dir='NIFTYFITDIR')


Test patch:
diff --git a/nipype/interfaces/base/tests/test_specs.py b/nipype/interfaces/base/tests/test_specs.py
--- a/nipype/interfaces/base/tests/test_specs.py
+++ b/nipype/interfaces/base/tests/test_specs.py
@@ -274,6 +274,55 @@ class TestCycle(nib.CommandLine):
     assert '%s_generated_mootpl' % nme in res
 
 
+def test_namesource_constraints(setup_file):
+    tmp_infile = setup_file
+    tmpd, nme, ext = split_filename(tmp_infile)
+
+    class constrained_spec(nib.CommandLineInputSpec):
+        in_file = nib.File(argstr="%s", position=1)
+        threshold = traits.Float(
+            argstr="%g",
+            xor=['mask_file'],
+            position=2)
+        mask_file = nib.File(
+            argstr="%s",
+            name_source=['in_file'],
+            name_template='%s_mask',
+            keep_extension=True,
+            xor=['threshold'],
+            position=2)
+        out_file1 = nib.File(
+            argstr="%s",
+            name_source=['in_file'],
+            name_template='%s_out1',
+            keep_extension=True,
+            position=3)
+        out_file2 = nib.File(
+            argstr="%s",
+            name_source=['in_file'],
+            name_template='%s_out2',
+            keep_extension=True,
+            requires=['threshold'],
+            position=4)
+
+    class TestConstrained(nib.CommandLine):
+        _cmd = "mycommand"
+        input_spec = constrained_spec
+
+    tc = TestConstrained()
+
+    # name_source undefined, so template traits remain undefined
+    assert tc.cmdline == 'mycommand'
+
+    # mask_file and out_file1 enabled by name_source definition
+    tc.inputs.in_file = os.path.basename(tmp_infile)
+    assert tc.cmdline == 'mycommand foo.txt foo_mask.txt foo_out1.txt'
+
+    # mask_file disabled by threshold, out_file2 enabled by threshold
+    tc.inputs.threshold = 10.
+    assert tc.cmdline == 'mycommand foo.txt 10 foo_out1.txt foo_out2.txt'
+
+
 def test_TraitedSpec_withFile(setup_file):
     tmp_infile = setup_file
     tmpd, nme = os.path.split(tmp_infile)
diff --git a/nipype/interfaces/fsl/tests/test_preprocess.py b/nipype/interfaces/fsl/tests/test_preprocess.py
--- a/nipype/interfaces/fsl/tests/test_preprocess.py
+++ b/nipype/interfaces/fsl/tests/test_preprocess.py
@@ -313,6 +313,7 @@ def test_flirt(setup_flirt):
         os.path.join(os.getcwd(), flirter.inputs.out_file)
     assert outs['out_matrix_file'] == \
         os.path.join(os.getcwd(), flirter.inputs.out_matrix_file)
+    assert not isdefined(flirter.inputs.out_log)
 
 
 # Mcflirt