Skip to content

Commit 20f0444

Browse files
authored
Merge pull request #1828 from oesteban/enh/WorkflowInterface
[ENH] Refactoring of nipype.interfaces.utility
2 parents 86cd8ff + b107870 commit 20f0444

19 files changed

+429
-352
lines changed

CHANGES

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
Upcoming release 0.13
22
=====================
33

4+
* ENH: Refactoring of nipype.interfaces.utility (https://github.com/nipy/nipype/pull/1828)
45
* FIX: CircleCI were failing silently. Some fixes to tests (https://github.com/nipy/nipype/pull/1833)
56
* FIX: Issues in Docker image permissions, and docker documentation (https://github.com/nipy/nipype/pull/1825)
67
* ENH: Revised all Dockerfiles and automated deployment to Docker Hub

nipype/interfaces/freesurfer/tests/test_utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
# vi: set ft=python sts=4 ts=4 sw=4 et:
44
from __future__ import print_function, division, unicode_literals, absolute_import
55
from builtins import open
6-
import os
7-
6+
import os, os.path as op
87
import pytest
9-
from nipype.testing.fixtures import (create_files_in_directory_plus_dummy_file,
8+
from nipype.testing.fixtures import (create_files_in_directory_plus_dummy_file,
109
create_surf_file_in_directory)
1110

1211
from nipype.interfaces.base import TraitError

nipype/interfaces/utility/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# -*- coding: utf-8 -*-
2+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
3+
# vi: set ft=python sts=4 ts=4 sw=4 et:
4+
"""
5+
Package contains interfaces for using existing functionality in other packages
6+
7+
Requires Packages to be installed
8+
"""
9+
10+
from .base import (IdentityInterface, Rename, Select, Split, Merge,
11+
AssertEqual)
12+
from .csv import CSVReader
13+
from .wrappers import Function

nipype/interfaces/utility.py renamed to nipype/interfaces/utility/base.py

Lines changed: 21 additions & 267 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
# -*- coding: utf-8 -*-
22
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
33
# vi: set ft=python sts=4 ts=4 sw=4 et:
4-
"""Various utilities
4+
"""
5+
Various utilities
6+
7+
Change directory to provide relative paths for doctests
8+
>>> import os
9+
>>> filepath = os.path.dirname(os.path.realpath(__file__))
10+
>>> datadir = os.path.realpath(os.path.join(filepath,
11+
... '../../testing/data'))
12+
>>> os.chdir(datadir)
513
6-
Change directory to provide relative paths for doctests
7-
>>> import os
8-
>>> filepath = os.path.dirname( os.path.realpath( __file__ ) )
9-
>>> datadir = os.path.realpath(os.path.join(filepath, '../testing/data'))
10-
>>> os.chdir(datadir)
1114
"""
1215
from __future__ import print_function, division, unicode_literals, absolute_import
13-
from builtins import zip, range, str, open
16+
from builtins import range
1417

1518
from future import standard_library
1619
standard_library.install_aliases()
@@ -20,22 +23,12 @@
2023
import numpy as np
2124
import nibabel as nb
2225

23-
from nipype import logging
24-
from .base import (traits, TraitedSpec, DynamicTraitedSpec, File,
25-
Undefined, isdefined, OutputMultiPath, runtime_profile,
26-
InputMultiPath, BaseInterface, BaseInterfaceInputSpec)
27-
from .io import IOBase, add_traits
28-
from ..utils.filemanip import (filename_to_list, copyfile, split_filename)
29-
from ..utils.misc import getsource, create_function_from_source
30-
31-
logger = logging.getLogger('interface')
32-
if runtime_profile:
33-
try:
34-
import psutil
35-
except ImportError as exc:
36-
logger.info('Unable to import packages needed for runtime profiling. '\
37-
'Turning off runtime profiler. Reason: %s' % exc)
38-
runtime_profile = False
26+
from ..base import (traits, TraitedSpec, DynamicTraitedSpec, File,
27+
Undefined, isdefined, OutputMultiPath, InputMultiPath,
28+
BaseInterface, BaseInterfaceInputSpec, Str)
29+
from ..io import IOBase, add_traits
30+
from ...utils.filemanip import filename_to_list, copyfile, split_filename
31+
3932

4033
class IdentityInterface(IOBase):
4134
"""Basic interface class generates identity mappings
@@ -163,11 +156,10 @@ class RenameInputSpec(DynamicTraitedSpec):
163156
in_file = File(exists=True, mandatory=True, desc="file to rename")
164157
keep_ext = traits.Bool(desc=("Keep in_file extension, replace "
165158
"non-extension component of name"))
166-
format_string = traits.String(mandatory=True,
167-
desc=("Python formatting string for output "
168-
"template"))
169-
parse_string = traits.String(desc=("Python regexp parse string to define "
170-
"replacement inputs"))
159+
format_string = Str(mandatory=True,
160+
desc="Python formatting string for output template")
161+
parse_string = Str(desc="Python regexp parse string to define "
162+
"replacement inputs")
171163
use_fullpath = traits.Bool(False, usedefault=True,
172164
desc="Use full path as input to regex parser")
173165

@@ -191,6 +183,7 @@ class Rename(IOBase):
191183
192184
Examples
193185
--------
186+
194187
>>> from nipype.interfaces.utility import Rename
195188
>>> rename1 = Rename()
196189
>>> rename1.inputs.in_file = "zstat1.nii.gz"
@@ -357,165 +350,6 @@ def _list_outputs(self):
357350
return outputs
358351

359352

360-
class FunctionInputSpec(DynamicTraitedSpec, BaseInterfaceInputSpec):
361-
function_str = traits.Str(mandatory=True, desc='code for function')
362-
363-
364-
class Function(IOBase):
365-
"""Runs arbitrary function as an interface
366-
367-
Examples
368-
--------
369-
370-
>>> func = 'def func(arg1, arg2=5): return arg1 + arg2'
371-
>>> fi = Function(input_names=['arg1', 'arg2'], output_names=['out'])
372-
>>> fi.inputs.function_str = func
373-
>>> res = fi.run(arg1=1)
374-
>>> res.outputs.out
375-
6
376-
377-
"""
378-
379-
input_spec = FunctionInputSpec
380-
output_spec = DynamicTraitedSpec
381-
382-
def __init__(self, input_names, output_names, function=None, imports=None,
383-
**inputs):
384-
"""
385-
386-
Parameters
387-
----------
388-
389-
input_names: single str or list
390-
names corresponding to function inputs
391-
output_names: single str or list
392-
names corresponding to function outputs.
393-
has to match the number of outputs
394-
function : callable
395-
callable python object. must be able to execute in an
396-
isolated namespace (possibly in concert with the ``imports``
397-
parameter)
398-
imports : list of strings
399-
list of import statements that allow the function to execute
400-
in an otherwise empty namespace
401-
"""
402-
403-
super(Function, self).__init__(**inputs)
404-
if function:
405-
if hasattr(function, '__call__'):
406-
try:
407-
self.inputs.function_str = getsource(function)
408-
except IOError:
409-
raise Exception('Interface Function does not accept '
410-
'function objects defined interactively '
411-
'in a python session')
412-
elif isinstance(function, (str, bytes)):
413-
self.inputs.function_str = function
414-
else:
415-
raise Exception('Unknown type of function')
416-
self.inputs.on_trait_change(self._set_function_string,
417-
'function_str')
418-
self._input_names = filename_to_list(input_names)
419-
self._output_names = filename_to_list(output_names)
420-
add_traits(self.inputs, [name for name in self._input_names])
421-
self.imports = imports
422-
self._out = {}
423-
for name in self._output_names:
424-
self._out[name] = None
425-
426-
def _set_function_string(self, obj, name, old, new):
427-
if name == 'function_str':
428-
if hasattr(new, '__call__'):
429-
function_source = getsource(new)
430-
elif isinstance(new, (str, bytes)):
431-
function_source = new
432-
self.inputs.trait_set(trait_change_notify=False,
433-
**{'%s' % name: function_source})
434-
435-
def _add_output_traits(self, base):
436-
undefined_traits = {}
437-
for key in self._output_names:
438-
base.add_trait(key, traits.Any)
439-
undefined_traits[key] = Undefined
440-
base.trait_set(trait_change_notify=False, **undefined_traits)
441-
return base
442-
443-
def _run_interface(self, runtime):
444-
# Get workflow logger for runtime profile error reporting
445-
from nipype import logging
446-
logger = logging.getLogger('workflow')
447-
448-
# Create function handle
449-
function_handle = create_function_from_source(self.inputs.function_str,
450-
self.imports)
451-
452-
# Wrapper for running function handle in multiprocessing.Process
453-
# Can catch exceptions and report output via multiprocessing.Queue
454-
def _function_handle_wrapper(queue, **kwargs):
455-
try:
456-
out = function_handle(**kwargs)
457-
queue.put(out)
458-
except Exception as exc:
459-
queue.put(exc)
460-
461-
# Get function args
462-
args = {}
463-
for name in self._input_names:
464-
value = getattr(self.inputs, name)
465-
if isdefined(value):
466-
args[name] = value
467-
468-
# Profile resources if set
469-
if runtime_profile:
470-
from nipype.interfaces.base import get_max_resources_used
471-
import multiprocessing
472-
# Init communication queue and proc objs
473-
queue = multiprocessing.Queue()
474-
proc = multiprocessing.Process(target=_function_handle_wrapper,
475-
args=(queue,), kwargs=args)
476-
477-
# Init memory and threads before profiling
478-
mem_mb = 0
479-
num_threads = 0
480-
481-
# Start process and profile while it's alive
482-
proc.start()
483-
while proc.is_alive():
484-
mem_mb, num_threads = \
485-
get_max_resources_used(proc.pid, mem_mb, num_threads,
486-
pyfunc=True)
487-
488-
# Get result from process queue
489-
out = queue.get()
490-
# If it is an exception, raise it
491-
if isinstance(out, Exception):
492-
raise out
493-
494-
# Function ran successfully, populate runtime stats
495-
setattr(runtime, 'runtime_memory_gb', mem_mb / 1024.0)
496-
setattr(runtime, 'runtime_threads', num_threads)
497-
else:
498-
out = function_handle(**args)
499-
500-
if len(self._output_names) == 1:
501-
self._out[self._output_names[0]] = out
502-
else:
503-
if isinstance(out, tuple) and (len(out) != len(self._output_names)):
504-
raise RuntimeError('Mismatch in number of expected outputs')
505-
506-
else:
507-
for idx, name in enumerate(self._output_names):
508-
self._out[name] = out[idx]
509-
510-
return runtime
511-
512-
def _list_outputs(self):
513-
outputs = self._outputs().get()
514-
for key in self._output_names:
515-
outputs[key] = self._out[key]
516-
return outputs
517-
518-
519353
class AssertEqualInputSpec(BaseInterfaceInputSpec):
520354
volume1 = File(exists=True, mandatory=True)
521355
volume2 = File(exists=True, mandatory=True)
@@ -532,83 +366,3 @@ def _run_interface(self, runtime):
532366
if not np.all(data1 == data2):
533367
raise RuntimeError('Input images are not exactly equal')
534368
return runtime
535-
536-
537-
class CSVReaderInputSpec(DynamicTraitedSpec, TraitedSpec):
538-
in_file = File(exists=True, mandatory=True, desc='Input comma-seperated value (CSV) file')
539-
header = traits.Bool(False, usedefault=True, desc='True if the first line is a column header')
540-
541-
542-
class CSVReader(BaseInterface):
543-
"""
544-
Examples
545-
--------
546-
547-
>>> reader = CSVReader() # doctest: +SKIP
548-
>>> reader.inputs.in_file = 'noHeader.csv' # doctest: +SKIP
549-
>>> out = reader.run() # doctest: +SKIP
550-
>>> out.outputs.column_0 == ['foo', 'bar', 'baz'] # doctest: +SKIP
551-
True
552-
>>> out.outputs.column_1 == ['hello', 'world', 'goodbye'] # doctest: +SKIP
553-
True
554-
>>> out.outputs.column_2 == ['300.1', '5', '0.3'] # doctest: +SKIP
555-
True
556-
557-
>>> reader = CSVReader() # doctest: +SKIP
558-
>>> reader.inputs.in_file = 'header.csv' # doctest: +SKIP
559-
>>> reader.inputs.header = True # doctest: +SKIP
560-
>>> out = reader.run() # doctest: +SKIP
561-
>>> out.outputs.files == ['foo', 'bar', 'baz'] # doctest: +SKIP
562-
True
563-
>>> out.outputs.labels == ['hello', 'world', 'goodbye'] # doctest: +SKIP
564-
True
565-
>>> out.outputs.erosion == ['300.1', '5', '0.3'] # doctest: +SKIP
566-
True
567-
568-
"""
569-
input_spec = CSVReaderInputSpec
570-
output_spec = DynamicTraitedSpec
571-
_always_run = True
572-
573-
def _append_entry(self, outputs, entry):
574-
for key, value in zip(self._outfields, entry):
575-
outputs[key].append(value)
576-
return outputs
577-
578-
def _parse_line(self, line):
579-
line = line.replace('\n', '')
580-
entry = [x.strip() for x in line.split(',')]
581-
return entry
582-
583-
def _get_outfields(self):
584-
with open(self.inputs.in_file, 'r') as fid:
585-
entry = self._parse_line(fid.readline())
586-
if self.inputs.header:
587-
self._outfields = tuple(entry)
588-
else:
589-
self._outfields = tuple(['column_' + str(x) for x in range(len(entry))])
590-
return self._outfields
591-
592-
def _run_interface(self, runtime):
593-
self._get_outfields()
594-
return runtime
595-
596-
def _outputs(self):
597-
return self._add_output_traits(super(CSVReader, self)._outputs())
598-
599-
def _add_output_traits(self, base):
600-
return add_traits(base, self._get_outfields())
601-
602-
def _list_outputs(self):
603-
outputs = self.output_spec().get()
604-
isHeader = True
605-
for key in self._outfields:
606-
outputs[key] = [] # initialize outfields
607-
with open(self.inputs.in_file, 'r') as fid:
608-
for line in fid.readlines():
609-
if self.inputs.header and isHeader: # skip header line
610-
isHeader = False
611-
continue
612-
entry = self._parse_line(line)
613-
outputs = self._append_entry(outputs, entry)
614-
return outputs

0 commit comments

Comments
 (0)