diff --git a/CHANGES b/CHANGES index ca43e827e1..ecf067f8af 100644 --- a/CHANGES +++ b/CHANGES @@ -2,6 +2,7 @@ Next release ============ * ENH: Dropped support for now 7 years old Python 2.6 (https://github.com/nipy/nipype/pull/1069) * FIX: terminal_output is not mandatory anymore (https://github.com/nipy/nipype/pull/1070) +* ENH: Added "nipype_cmd" tool for running interfaces from the command line (https://github.com/nipy/nipype/pull/795) * FIX: Fixed Camino output naming (https://github.com/nipy/nipype/pull/1061) * ENH: Add the average distance to ErrorMap (https://github.com/nipy/nipype/pull/1039) * ENH: Inputs with name_source can be now chained in cascade (https://github.com/nipy/nipype/pull/938) diff --git a/bin/nipype_cmd b/bin/nipype_cmd new file mode 100755 index 0000000000..bdedb0789f --- /dev/null +++ b/bin/nipype_cmd @@ -0,0 +1,8 @@ +#!/usr/bin/env python +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +import sys +from nipype.utils.nipype_cmd import main + +if __name__ == '__main__': + main(sys.argv) diff --git a/doc/users/index.rst b/doc/users/index.rst index 8ecbdf2e08..c5ebbae1df 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -37,6 +37,7 @@ saving_workflows spmmcr mipav + nipypecmd diff --git a/doc/users/nipypecmd.rst b/doc/users/nipypecmd.rst new file mode 100644 index 0000000000..2fa86de840 --- /dev/null +++ b/doc/users/nipypecmd.rst @@ -0,0 +1,68 @@ +.. _nipypecmd: + +============================================================ +Running Nipype Interfaces from the command line (nipype_cmd) +============================================================ + +The primary use of Nipype_ is to build automated non-interactive pipelines. +However, sometimes there is a need to run some interfaces quickly from the command line. +This is especially useful when running Interfaces wrapping code that does not have +command line equivalents (nipy or SPM). Being able to run Nipype interfaces opens new +possibilities such as inclusion of SPM processing steps in bash scripts. + +To run Nipype Interafces you need to use the nipype_cmd tool that should already be installed. +The tool allows you to list Interfaces available in a certain package: + +.. testcode:: + + + $nipype_cmd nipype.interfaces.nipy + + Available Interfaces: + SpaceTimeRealigner + Similarity + ComputeMask + FitGLM + EstimateContrast + FmriRealign4d + +After selecting a particular Interface you can learn what inputs it requires: + +.. testcode:: + + + $nipype_cmd nipype.interfaces.nipy ComputeMask --help + + usage:nipype_cmd nipype.interfaces.nipy ComputeMask [-h] [--M M] [--cc CC] + [--ignore_exception IGNORE_EXCEPTION] + [--m M] + [--reference_volume REFERENCE_VOLUME] + mean_volume + + Run ComputeMask + + positional arguments: + mean_volume mean EPI image, used to compute the threshold for the + mask + + optional arguments: + -h, --help show this help message and exit + --M M upper fraction of the histogram to be discarded + --cc CC Keep only the largest connected component + --ignore_exception IGNORE_EXCEPTION + Print an error message instead of throwing an + exception in case the interface fails to run + --m M lower fraction of the histogram to be discarded + --reference_volume REFERENCE_VOLUME + reference volume used to compute the mask. If none is + give, the mean volume is used. + +Finally you can run run the Interface: + +.. testcode:: + + $nipype_cmd nipype.interfaces.nipy ComputeMask mean.nii.gz + +All that from the command line without having to start python interpreter manually. + +.. include:: ../links_names.txt diff --git a/nipype/utils/nipype_cmd.py b/nipype/utils/nipype_cmd.py new file mode 100644 index 0000000000..2b514e54d8 --- /dev/null +++ b/nipype/utils/nipype_cmd.py @@ -0,0 +1,72 @@ +import os +import argparse +import inspect +import sys +from nipype.interfaces.base import Interface + + +def listClasses(module=None): + if module: + __import__(module) + pkg = sys.modules[module] + print "Available Interfaces:" + for k,v in pkg.__dict__.items(): + if inspect.isclass(v) and issubclass(v, Interface): + print "\t%s"%k + +def add_options(parser=None, module=None, function=None): + interface = None + if parser and module and function: + __import__(module) + interface = getattr(sys.modules[module],function)() + + inputs = interface.input_spec() + for name, spec in sorted(interface.inputs.traits(transient=None).items()): + desc = "\n".join(interface._get_trait_desc(inputs, name, spec))[len(name)+2:] + if hasattr(spec, "mandatory") and spec.mandatory: + parser.add_argument(name, help=desc) + else: + parser.add_argument("--%s"%name, dest=name, + help=desc) + return parser, interface + +def run_instance(interface, options): + if interface: + print "setting function inputs" + + for input_name, _ in interface.inputs.items(): + if getattr(options, input_name) != None: + value = getattr(options, input_name) + #traits cannot cast from string to float or int + try: + value = float(value) + except: + pass + + try: + setattr(interface.inputs, input_name, + value) + except ValueError, e: + print "Error when setting the value of %s: '%s'"%(input_name, str(e)) + + print interface.inputs + res = interface.run() + print res.outputs + + +def main(argv): + + if len(argv) == 2 and not argv[1].startswith("-"): + listClasses(argv[1]) + sys.exit(0) + + parser = argparse.ArgumentParser(description='Nipype interface runner', prog=argv[0]) + parser.add_argument("module", type=str, help="Module name") + parser.add_argument("interface", type=str, help="Interface name") + parsed = parser.parse_args(args=argv[1:3]) + + _, prog = os.path.split(argv[0]) + interface_parser = argparse.ArgumentParser(description="Run %s"%parsed.interface, prog=" ".join([prog] + argv[1:3])) + interface_parser, interface = add_options(interface_parser, parsed.module, parsed.interface) + args = interface_parser.parse_args(args=argv[3:]) + run_instance(interface, args) \ No newline at end of file diff --git a/nipype/utils/tests/test_cmd.py b/nipype/utils/tests/test_cmd.py new file mode 100644 index 0000000000..55347ff4b9 --- /dev/null +++ b/nipype/utils/tests/test_cmd.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python + +from StringIO import StringIO +import unittest, sys +from nipype.utils import nipype_cmd +from contextlib import contextmanager + +@contextmanager +def capture_sys_output(): + caputure_out, capture_err = StringIO(), StringIO() + current_out, current_err = sys.stdout, sys.stderr + try: + sys.stdout, sys.stderr = caputure_out, capture_err + yield caputure_out, capture_err + finally: + sys.stdout, sys.stderr = current_out, current_err + + +class TestNipypeCMD(unittest.TestCase): + + def test_main_returns_2_on_empty(self): + with self.assertRaises(SystemExit) as cm: + with capture_sys_output() as (stdout, stderr): + nipype_cmd.main(['nipype_cmd']) + + exit_exception = cm.exception + self.assertEqual(exit_exception.code, 2) + + self.assertEqual(stderr.getvalue(), +"""usage: nipype_cmd [-h] module interface +nipype_cmd: error: too few arguments +""") + self.assertEqual(stdout.getvalue(), '') + + def test_main_returns_0_on_help(self): + with self.assertRaises(SystemExit) as cm: + with capture_sys_output() as (stdout, stderr): + nipype_cmd.main(['nipype_cmd', '-h']) + + exit_exception = cm.exception + self.assertEqual(exit_exception.code, 0) + + self.assertEqual(stderr.getvalue(), '') + self.assertEqual(stdout.getvalue(), +"""usage: nipype_cmd [-h] module interface + +Nipype interface runner + +positional arguments: + module Module name + interface Interface name + +optional arguments: + -h, --help show this help message and exit +""") + + def test_list_nipy_interfacesp(self): + with self.assertRaises(SystemExit) as cm: + with capture_sys_output() as (stdout, stderr): + nipype_cmd.main(['nipype_cmd', 'nipype.interfaces.nipy']) + + exit_exception = cm.exception + self.assertEqual(exit_exception.code, 0) + + self.assertEqual(stderr.getvalue(), '') + self.assertEqual(stdout.getvalue(), +"""Available Interfaces: + SpaceTimeRealigner + Similarity + ComputeMask + FitGLM + EstimateContrast + FmriRealign4d +""") + + def test_run_4d_realign_without_arguments(self): + with self.assertRaises(SystemExit) as cm: + with capture_sys_output() as (stdout, stderr): + nipype_cmd.main(['nipype_cmd', 'nipype.interfaces.nipy', 'FmriRealign4d']) + + exit_exception = cm.exception + self.assertEqual(exit_exception.code, 2) + + self.assertEqual(stderr.getvalue(), +"""usage: nipype_cmd nipype.interfaces.nipy FmriRealign4d [-h] + [--between_loops BETWEEN_LOOPS] + [--ignore_exception IGNORE_EXCEPTION] + [--loops LOOPS] + [--slice_order SLICE_ORDER] + [--speedup SPEEDUP] + [--start START] + [--time_interp TIME_INTERP] + [--tr_slices TR_SLICES] + in_file tr +nipype_cmd nipype.interfaces.nipy FmriRealign4d: error: too few arguments +""") + self.assertEqual(stdout.getvalue(), '') + + def test_run_4d_realign_help(self): + with self.assertRaises(SystemExit) as cm: + with capture_sys_output() as (stdout, stderr): + nipype_cmd.main(['nipype_cmd', 'nipype.interfaces.nipy', 'FmriRealign4d', '-h']) + + exit_exception = cm.exception + self.assertEqual(exit_exception.code, 0) + + self.assertEqual(stderr.getvalue(), '') + self.assertTrue("Run FmriRealign4d" in stdout.getvalue()) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tools/run_interface.py b/tools/run_interface.py deleted file mode 100644 index ea9fceb825..0000000000 --- a/tools/run_interface.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -"""Script to auto-generate our API docs. -""" -# stdlib imports -import os -from optparse import OptionParser -import sys - -def listClasses(module=None): - if module: - __import__(module) - pkg = sys.modules[module] - print "Available functions:" - for k,v in pkg.__dict__.items(): - if 'class' in str(v) and k != '__builtins__': - print "\t%s"%k - -def add_options(parser=None, module=None, function=None): - interface = None - if parser and module and function: - __import__(module) - interface = getattr(sys.modules[module],function)() - - for k,v in interface.inputs.iteritems(): - parser.add_option("-%s"%k[0], "--%s"%k, dest="IXI%s"%k, - metavar=k, - action='store',type='string', - help="you need help?",default='') - return parser, interface - -def run_instance(interface, options): - if interface: - print "setting function inputs" - for k,v in interface.inputs.iteritems(): - optionskey = ''.join(('IXI',k)) - if hasattr(options, optionskey): - setattr(interface.inputs, k, - getattr(options, optionskey)) - print interface.inputs - print "not really running anything" - -def get_modfunc(args): - module = None - function = None - posargs = [] - skip = False - for a in args: - if skip: - skip = False - continue - if a.startswith('--'): - pass - elif a.startswith('-'): - skip = True - else: - posargs.append(a) - if posargs: - module = posargs[0] - if len(posargs)==2: - function = posargs[1] - return module, function - -def parse_args(): - usage = "usage: %prog [options] module function" - parser = OptionParser(usage=usage,version="%prog 1.0", - conflict_handler="resolve") - parser.add_option("--run", dest="run", - action='store_true',help="Execute", - default=False) - - module, function = get_modfunc(sys.argv[1:]) - parser, interface = add_options(parser, module, function) - (options, args) = parser.parse_args() - if options.run and interface: - #assign inputs - run_instance(interface, options) - else: - parser.print_help() - if module and not function: - listClasses(module) - parser.exit() - - -#***************************************************************************** -if __name__ == '__main__': - parse_args()