Skip to content

Commit b4f9131

Browse files
authored
Merge pull request nipy#3054 from stilley2/feature/Rename_copy_trait
ENH: Add "ExportFile" interface as simple alternative to "DataSink"
2 parents 57f1569 + 3695d36 commit b4f9131

File tree

4 files changed

+114
-4
lines changed

4 files changed

+114
-4
lines changed

nipype/interfaces/io.py

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@
3232
from .. import config, logging
3333
from ..utils.filemanip import (
3434
copyfile, simplify_list, ensure_list,
35-
get_related_files)
35+
get_related_files, split_filename,
36+
FileExistsError)
3637
from ..utils.misc import human_order_sorted, str2bool
3738
from .base import (
3839
TraitedSpec, traits, Str, File, Directory, BaseInterface, InputMultiPath,
3940
isdefined, OutputMultiPath, DynamicTraitedSpec, Undefined, BaseInterfaceInputSpec,
40-
LibraryBaseInterface)
41+
LibraryBaseInterface, SimpleInterface)
4142

4243
iflogger = logging.getLogger('nipype.interface')
4344

@@ -2863,3 +2864,51 @@ def _list_outputs(self):
28632864

28642865
def _add_output_traits(self, base):
28652866
return add_traits(base, list(self.inputs.output_query.keys()))
2867+
2868+
2869+
class ExportFileInputSpec(BaseInterfaceInputSpec):
2870+
in_file = File(exists=True, mandatory=True, desc='Input file name')
2871+
out_file = File(mandatory=True, desc='Output file name')
2872+
check_extension = traits.Bool(True, desc='Ensure that the input and output file extensions match')
2873+
clobber = traits.Bool(desc='Permit overwriting existing files')
2874+
2875+
2876+
class ExportFileOutputSpec(TraitedSpec):
2877+
out_file = File(exists=True, desc='Output file name')
2878+
2879+
2880+
class ExportFile(SimpleInterface):
2881+
""" Export a file to an absolute path
2882+
2883+
This interface copies an input file to a named output file.
2884+
This is useful to save individual files to a specific location,
2885+
instead of more flexible interfaces like DataSink.
2886+
2887+
Examples
2888+
--------
2889+
2890+
>>> from nipype.interfaces.io import ExportFile
2891+
>>> import os.path as op
2892+
>>> ef = ExportFile()
2893+
>>> ef.inputs.in_file = "T1.nii.gz"
2894+
>>> os.mkdir("output_folder")
2895+
>>> ef.inputs.out_file = op.abspath("output_folder/sub1_out.nii.gz")
2896+
>>> res = ef.run()
2897+
>>> os.path.exists(res.outputs.out_file)
2898+
True
2899+
2900+
"""
2901+
input_spec = ExportFileInputSpec
2902+
output_spec = ExportFileOutputSpec
2903+
2904+
def _run_interface(self, runtime):
2905+
if not self.inputs.clobber and op.exists(self.inputs.out_file):
2906+
raise FileExistsError(self.inputs.out_file)
2907+
if not op.isabs(self.inputs.out_file):
2908+
raise ValueError('Out_file must be an absolute path.')
2909+
if (self.inputs.check_extension and
2910+
split_filename(self.inputs.in_file)[2] != split_filename(self.inputs.out_file)[2]):
2911+
raise RuntimeError('%s and %s have different extensions' % (self.inputs.in_file, self.inputs.out_file))
2912+
shutil.copy(str(self.inputs.in_file), str(self.inputs.out_file))
2913+
self._results['out_file'] = self.inputs.out_file
2914+
return runtime
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from __future__ import unicode_literals
3+
from ..io import ExportFile
4+
5+
6+
def test_ExportFile_inputs():
7+
input_map = dict(
8+
check_extension=dict(),
9+
clobber=dict(),
10+
in_file=dict(
11+
extensions=None,
12+
mandatory=True,
13+
),
14+
out_file=dict(
15+
extensions=None,
16+
mandatory=True,
17+
),
18+
)
19+
inputs = ExportFile.input_spec()
20+
21+
for key, metadata in list(input_map.items()):
22+
for metakey, value in list(metadata.items()):
23+
assert getattr(inputs.traits()[key], metakey) == value
24+
def test_ExportFile_outputs():
25+
output_map = dict(out_file=dict(extensions=None, ), )
26+
outputs = ExportFile.output_spec()
27+
28+
for key, metadata in list(output_map.items()):
29+
for metakey, value in list(metadata.items()):
30+
assert getattr(outputs.traits()[key], metakey) == value

nipype/interfaces/tests/test_io.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import nipype.interfaces.io as nio
2121
from nipype.interfaces.base.traits_extension import isdefined
2222
from nipype.interfaces.base import Undefined, TraitError
23-
from nipype.utils.filemanip import dist_is_editable
23+
from nipype.utils.filemanip import dist_is_editable, FileExistsError
2424

2525
# Check for boto
2626
noboto = False
@@ -680,3 +680,26 @@ def _mock_get_ssh_client(self):
680680
.check(file=True, exists=True)) # exists?
681681

682682
old_cwd.chdir()
683+
684+
685+
def test_ExportFile(tmp_path):
686+
testin = tmp_path / 'in.txt'
687+
testin.write_text('test string')
688+
i = nio.ExportFile()
689+
i.inputs.in_file = str(testin)
690+
i.inputs.out_file = str(tmp_path / 'out.tsv')
691+
i.inputs.check_extension = True
692+
with pytest.raises(RuntimeError):
693+
i.run()
694+
i.inputs.check_extension = False
695+
i.run()
696+
assert (tmp_path / 'out.tsv').read_text() == 'test string'
697+
i.inputs.out_file = str(tmp_path / 'out.txt')
698+
i.inputs.check_extension = True
699+
i.run()
700+
assert (tmp_path / 'out.txt').read_text() == 'test string'
701+
with pytest.raises(FileExistsError):
702+
i.run()
703+
i.inputs.clobber = True
704+
i.run()
705+
assert (tmp_path / 'out.txt').read_text() == 'test string'

nipype/utils/filemanip.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
PY3 = sys.version_info[0] >= 3
4242

4343
try:
44-
from builtins import FileNotFoundError
44+
from builtins import FileNotFoundError, FileExistsError
4545
except ImportError: # PY27
4646
class FileNotFoundError(OSError): # noqa
4747
"""Defines the exception for Python 2."""
@@ -51,6 +51,14 @@ def __init__(self, path):
5151
super(FileNotFoundError, self).__init__(
5252
2, 'No such file or directory', '%s' % path)
5353

54+
class FileExistsError(OSError): # noqa
55+
"""Defines the exception for Python 2."""
56+
57+
def __init__(self, path):
58+
"""Initialize the exception."""
59+
super(FileExistsError, self).__init__(
60+
17, 'File or directory exists', '%s' % path)
61+
5462

5563
USING_PATHLIB2 = False
5664
try:

0 commit comments

Comments
 (0)