Repository: nipy/nipype

Issue:
dcmstack output file path
Hello, I am trying to specify an ouput file path for the `nipype.interfaces.dcmstack.DcmStack` interface, which should be in my data hierarchy and not in my script hierarchy.

I am doing this ~via:

```
from nipype.interfaces.dcmstack import DcmStack
stacker = DcmStack()
stacker.inputs.out_format = "/path/to/my/data"
```

stubbornly, dcmstack will export the resulting file to `/path/to/my/script/_path_to_my_data`

Can we fix this somehow? Right now I hav to export to my script directory and then move the file -.- which is a dodgy and slow way to fix this (different hard drives etc).

dcmstack output file path
Hello, I am trying to specify an ouput file path for the `nipype.interfaces.dcmstack.DcmStack` interface, which should be in my data hierarchy and not in my script hierarchy.

I am doing this ~via:

```
from nipype.interfaces.dcmstack import DcmStack
stacker = DcmStack()
stacker.inputs.out_format = "/path/to/my/data"
```

stubbornly, dcmstack will export the resulting file to `/path/to/my/script/_path_to_my_data`

Can we fix this somehow? Right now I hav to export to my script directory and then move the file -.- which is a dodgy and slow way to fix this (different hard drives etc).



Discussion:
Right now, I guess that's not possible: https://github.com/nipy/nipype/blob/44f75665bf4776d7e6ff63395254de3111da8ce5/nipype/interfaces/dcmstack.py#L76

A nice solution to the problem could be adding a bool switch to NiftiGeneratorBaseInputSpec indicating if the path is absolute or relative. Then, modify the line I've pointed out to choose between op.join or op.abspath.

and how would that switch get triggered?

@TheChymera I have written a fast patch, let me know if it works out for you. I'm not 100% sure this will work, but you'll need to check it out from my branch:

```
git remote add oesteban git@github.com:oesteban/nipype.git
git remote update
git pull oesteban fix/DCMStackOutput-1087
```

PS. When you use the interface, make sure you set the input `use_cwd=False`

Do I need to test this before the pull gets accepted? I am running the live nipype version, but setting up a development installation that works nicely with my package manager might take a while :-/

@oesteban how do I use this feature? 

I installed the live version of nipype, and still stuff gets exported to `/path/to/my/script/_path_to_my_data`.

I guess I have t speify the destination via somethign other than `stacker.inputs.out_format` -  but what?

try `use_cwd = False` for that interface

@satra so:

```
stacker.inputs.out_format = "/path/to/my/data"
stacker.inputs.use_cwd = False
```

?

yes

@satra tried it - that still exports the file to `/path/to/my/script/_path_to_my_data`

i looked at the code, and it looks like it's doing the correct thing. can you insert a debug breakpoint and step through. 

uhm, how do I do that? Can you maybe test this at your end as well?

I am just running the interface as:

```
            stacker.inputs.dicom_files = my_files_list
            stacker.inputs.embed_meta = True
            file_name = "EPIwhatever"
            stacker.inputs.out_format = /path/to/my/file
            stacker.inputs.use_cwd = False
            result = stacker.run()
            print(result.outputs.out_file)
```

And that prints `/path/to/my/script/path_to_my_file`, and puts the file there, as opposed to under the `/path/to/my/file` directory. By the way, how would the interface deal with an inexistent output path (i.e. one where some directories would need to be created) ?

ok, I see. There is also this function https://github.com/nipy/nipype/blob/df63a28152540ff95e7955403a9dc09f6f6e0876/nipype/interfaces/dcmstack.py#L36 converting slashes to underscores. @satra, what do you think of adding an `out_path` input that is cwd by default?

Right now, I guess that's not possible: https://github.com/nipy/nipype/blob/44f75665bf4776d7e6ff63395254de3111da8ce5/nipype/interfaces/dcmstack.py#L76

A nice solution to the problem could be adding a bool switch to NiftiGeneratorBaseInputSpec indicating if the path is absolute or relative. Then, modify the line I've pointed out to choose between op.join or op.abspath.

and how would that switch get triggered?

@TheChymera I have written a fast patch, let me know if it works out for you. I'm not 100% sure this will work, but you'll need to check it out from my branch:

```
git remote add oesteban git@github.com:oesteban/nipype.git
git remote update
git pull oesteban fix/DCMStackOutput-1087
```

PS. When you use the interface, make sure you set the input `use_cwd=False`

Do I need to test this before the pull gets accepted? I am running the live nipype version, but setting up a development installation that works nicely with my package manager might take a while :-/

@oesteban how do I use this feature? 

I installed the live version of nipype, and still stuff gets exported to `/path/to/my/script/_path_to_my_data`.

I guess I have t speify the destination via somethign other than `stacker.inputs.out_format` -  but what?

try `use_cwd = False` for that interface

@satra so:

```
stacker.inputs.out_format = "/path/to/my/data"
stacker.inputs.use_cwd = False
```

?

yes

@satra tried it - that still exports the file to `/path/to/my/script/_path_to_my_data`

i looked at the code, and it looks like it's doing the correct thing. can you insert a debug breakpoint and step through. 

uhm, how do I do that? Can you maybe test this at your end as well?

I am just running the interface as:

```
            stacker.inputs.dicom_files = my_files_list
            stacker.inputs.embed_meta = True
            file_name = "EPIwhatever"
            stacker.inputs.out_format = /path/to/my/file
            stacker.inputs.use_cwd = False
            result = stacker.run()
            print(result.outputs.out_file)
```

And that prints `/path/to/my/script/path_to_my_file`, and puts the file there, as opposed to under the `/path/to/my/file` directory. By the way, how would the interface deal with an inexistent output path (i.e. one where some directories would need to be created) ?

ok, I see. There is also this function https://github.com/nipy/nipype/blob/df63a28152540ff95e7955403a9dc09f6f6e0876/nipype/interfaces/dcmstack.py#L36 converting slashes to underscores. @satra, what do you think of adding an `out_path` input that is cwd by default?


Resolving patch:
diff --git a/nipype/interfaces/dcmstack.py b/nipype/interfaces/dcmstack.py
--- a/nipype/interfaces/dcmstack.py
+++ b/nipype/interfaces/dcmstack.py
@@ -9,8 +9,10 @@
 """
 
 from __future__ import absolute_import
-import os, string
-from os import path
+import os
+import string
+import errno
+from os import path as op
 from glob import glob
 from nipype.interfaces.base import (TraitedSpec,
                                     DynamicTraitedSpec,
@@ -33,6 +35,7 @@
 except ImportError:
     have_dcmstack = False
 
+
 def sanitize_path_comp(path_comp):
     result = []
     for char in path_comp:
@@ -48,8 +51,7 @@ class NiftiGeneratorBaseInputSpec(TraitedSpec):
                             "meta data to create the output filename(s)")
     out_ext = traits.Str('.nii.gz', usedefault=True,
                          desc="Determines output file type")
-    use_cwd = traits.Bool(True, usedefault=True,
-                          desc='use interface\'s current working directory')
+    out_path = Directory(desc='output path, current working directory if not set')
 
 
 class NiftiGeneratorBase(BaseInterface):
@@ -77,10 +79,20 @@ def _get_out_path(self, meta, idx=None):
         out_fn = (out_fmt % meta) + self.inputs.out_ext
         out_fn = sanitize_path_comp(out_fn)
 
-        if self.inputs.use_cwd:
-            return path.join(os.getcwd(), out_fn)
-        else:
-            return path.abspath(out_fn)
+        out_path = os.getcwd()
+        if isdefined(self.inputs.out_path):
+            out_path = op.abspath(self.inputs.out_path)
+
+            # now, mkdir -p $out_path
+            try:
+                os.makedirs(out_path)
+            except OSError as exc:  # Python >2.5
+                if exc.errno == errno.EEXIST and op.isdir(out_path):
+                    pass
+                else:
+                    raise
+
+        return op.join(out_path, out_fn)
 
 
 class DcmStackInputSpec(NiftiGeneratorBaseInputSpec):
@@ -100,6 +112,7 @@ class DcmStackInputSpec(NiftiGeneratorBaseInputSpec):
 class DcmStackOutputSpec(TraitedSpec):
     out_file = File(exists=True)
 
+
 class DcmStack(NiftiGeneratorBase):
     '''Create one Nifti file from a set of DICOM files. Can optionally embed
     meta data.
@@ -119,8 +132,8 @@ class DcmStack(NiftiGeneratorBase):
 
     def _get_filelist(self, trait_input):
         if isinstance(trait_input, six.string_types):
-            if path.isdir(trait_input):
-                return glob(path.join(trait_input, '*.dcm'))
+            if op.isdir(trait_input):
+                return glob(op.join(trait_input, '*.dcm'))
             else:
                 return glob(trait_input)
 
@@ -155,9 +168,11 @@ def _list_outputs(self):
         outputs["out_file"] = self.out_path
         return outputs
 
+
 class GroupAndStackOutputSpec(TraitedSpec):
     out_list = traits.List(desc="List of output nifti files")
 
+
 class GroupAndStack(DcmStack):
     '''Create (potentially) multiple Nifti files for a set of DICOM files.
     '''
@@ -185,6 +200,7 @@ def _list_outputs(self):
         outputs["out_list"] = self.out_list
         return outputs
 
+
 class LookupMetaInputSpec(TraitedSpec):
     in_file = File(mandatory=True,
                    exists=True,
@@ -197,6 +213,7 @@ class LookupMetaInputSpec(TraitedSpec):
                               "lookup and the values specify the output names")
                              )
 
+
 class LookupMeta(BaseInterface):
     '''Lookup meta data values from a Nifti with embeded meta data.
 
@@ -233,13 +250,13 @@ def _outputs(self):
             outputs.add_trait(out_name, traits.Any)
             undefined_traits[out_name] = Undefined
         outputs.trait_set(trait_change_notify=False, **undefined_traits)
-        #Not sure why this is needed
+        # Not sure why this is needed
         for out_name in self._meta_keys.values():
             _ = getattr(outputs, out_name)
         return outputs
 
     def _run_interface(self, runtime):
-        #If the 'meta_keys' input is a list, covert it to a dict
+        # If the 'meta_keys' input is a list, covert it to a dict
         self._make_name_map()
         nw = NiftiWrapper.from_filename(self.inputs.in_file)
         self.result = {}
@@ -253,6 +270,7 @@ def _list_outputs(self):
         outputs.update(self.result)
         return outputs
 
+
 class CopyMetaInputSpec(TraitedSpec):
     src_file = File(mandatory=True, exists=True)
     dest_file = File(mandatory=True, exists=True)
@@ -262,9 +280,11 @@ class CopyMetaInputSpec(TraitedSpec):
     exclude_classes = traits.List(desc="List of meta data "
                                   "classifications to exclude")
 
+
 class CopyMetaOutputSpec(TraitedSpec):
     dest_file = File(exists=True)
 
+
 class CopyMeta(BaseInterface):
     '''Copy meta data from one Nifti file to another. Useful for preserving
     meta data after some processing steps.'''
@@ -296,8 +316,8 @@ def _run_interface(self, runtime):
         dest.meta_ext.slice_dim = src.meta_ext.slice_dim
         dest.meta_ext.shape = src.meta_ext.shape
 
-        self.out_path = path.join(os.getcwd(),
-                                  path.basename(self.inputs.dest_file))
+        self.out_path = op.join(os.getcwd(),
+                                  op.basename(self.inputs.dest_file))
         dest.to_filename(self.out_path)
 
         return runtime
@@ -307,6 +327,7 @@ def _list_outputs(self):
         outputs['dest_file'] = self.out_path
         return outputs
 
+
 class MergeNiftiInputSpec(NiftiGeneratorBaseInputSpec):
     in_files = traits.List(mandatory=True,
                            desc="List of Nifti files to merge")
@@ -318,9 +339,11 @@ class MergeNiftiInputSpec(NiftiGeneratorBaseInputSpec):
                            "specified, the last singular or "
                            "non-existant dimension is used.")
 
+
 class MergeNiftiOutputSpec(TraitedSpec):
     out_file = File(exists=True, desc="Merged Nifti file")
 
+
 def make_key_func(meta_keys, index=None):
     def key_func(src_nii):
         result = [src_nii.get_meta(key, index) for key in meta_keys]
@@ -328,6 +351,7 @@ def key_func(src_nii):
 
     return key_func
 
+
 class MergeNifti(NiftiGeneratorBase):
     '''Merge multiple Nifti files into one. Merges together meta data
     extensions as well.'''
@@ -361,18 +385,23 @@ def _list_outputs(self):
         outputs['out_file'] = self.out_path
         return outputs
 
+
 class SplitNiftiInputSpec(NiftiGeneratorBaseInputSpec):
     in_file = File(exists=True, mandatory=True, desc="Nifti file to split")
     split_dim = traits.Int(desc="Dimension to split along. If not "
                            "specified, the last dimension is used.")
 
+
 class SplitNiftiOutputSpec(TraitedSpec):
     out_list = traits.List(File(exists=True),
                            desc="Split Nifti files")
 
+
 class SplitNifti(NiftiGeneratorBase):
-    '''Split one Nifti file into many along the specified dimension. Each
-    result has an updated meta data extension as well.'''
+    '''
+    Split one Nifti file into many along the specified dimension. Each
+    result has an updated meta data extension as well.
+    '''
     input_spec = SplitNiftiInputSpec
     output_spec = SplitNiftiOutputSpec
 


Test patch:
diff --git a/nipype/interfaces/tests/test_auto_DcmStack.py b/nipype/interfaces/tests/test_auto_DcmStack.py
--- a/nipype/interfaces/tests/test_auto_DcmStack.py
+++ b/nipype/interfaces/tests/test_auto_DcmStack.py
@@ -13,8 +13,7 @@ def test_DcmStack_inputs():
     out_ext=dict(usedefault=True,
     ),
     out_format=dict(),
-    use_cwd=dict(usedefault=True,
-    ),
+    out_path=dict(),
     )
     inputs = DcmStack.input_spec()
 
diff --git a/nipype/interfaces/tests/test_auto_GroupAndStack.py b/nipype/interfaces/tests/test_auto_GroupAndStack.py
--- a/nipype/interfaces/tests/test_auto_GroupAndStack.py
+++ b/nipype/interfaces/tests/test_auto_GroupAndStack.py
@@ -13,8 +13,7 @@ def test_GroupAndStack_inputs():
     out_ext=dict(usedefault=True,
     ),
     out_format=dict(),
-    use_cwd=dict(usedefault=True,
-    ),
+    out_path=dict(),
     )
     inputs = GroupAndStack.input_spec()
 
diff --git a/nipype/interfaces/tests/test_auto_MergeNifti.py b/nipype/interfaces/tests/test_auto_MergeNifti.py
--- a/nipype/interfaces/tests/test_auto_MergeNifti.py
+++ b/nipype/interfaces/tests/test_auto_MergeNifti.py
@@ -9,9 +9,8 @@ def test_MergeNifti_inputs():
     out_ext=dict(usedefault=True,
     ),
     out_format=dict(),
+    out_path=dict(),
     sort_order=dict(),
-    use_cwd=dict(usedefault=True,
-    ),
     )
     inputs = MergeNifti.input_spec()
 
diff --git a/nipype/interfaces/tests/test_auto_SplitNifti.py b/nipype/interfaces/tests/test_auto_SplitNifti.py
--- a/nipype/interfaces/tests/test_auto_SplitNifti.py
+++ b/nipype/interfaces/tests/test_auto_SplitNifti.py
@@ -8,9 +8,8 @@ def test_SplitNifti_inputs():
     out_ext=dict(usedefault=True,
     ),
     out_format=dict(),
+    out_path=dict(),
     split_dim=dict(),
-    use_cwd=dict(usedefault=True,
-    ),
     )
     inputs = SplitNifti.input_spec()