Skip to content

Commit 578970e

Browse files
committed
wip: add "nipype run" command
1 parent 4237c65 commit 578970e

File tree

5 files changed

+299
-73
lines changed

5 files changed

+299
-73
lines changed

nipype/scripts/cli.py

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,64 @@
11
#!python
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-
54
import click
65

6+
from .instance import list_interfaces
7+
from .utils import (CONTEXT_SETTINGS,
8+
UNKNOWN_OPTIONS,
9+
ExistingDirPath,
10+
ExistingFilePath,
11+
RegularExpression,
12+
PythonModule)
13+
714

8-
@click.group()
15+
# declare the CLI group
16+
@click.group(context_settings=CONTEXT_SETTINGS)
917
def cli():
1018
pass
1119

1220

13-
@cli.command()
14-
@click.argument('logdir', type=str)
15-
@click.option('-r', '--regex', type=str, default='*',
21+
@cli.command(context_settings=CONTEXT_SETTINGS)
22+
@click.argument('logdir', type=ExistingDirPath)
23+
@click.option('-r', '--regex', type=RegularExpression(), default='*',
1624
help='Regular expression to be searched in each traceback.')
1725
def search(logdir, regex):
1826
"""Search for tracebacks content.
1927
2028
Search for traceback inside a folder of nipype crash log files that match
2129
a given regular expression.
2230
23-
Examples:
24-
nipype search -d nipype/wd/log -r '.*subject123.*'
31+
Examples:\n
32+
nipype search nipype/wd/log -r '.*subject123.*'
2533
"""
26-
import re
2734
from .crash_files import iter_tracebacks
2835

29-
rex = re.compile(regex, re.IGNORECASE)
3036
for file, trace in iter_tracebacks(logdir):
31-
if rex.search(trace):
37+
if regex.search(trace):
3238
click.echo("-" * len(file))
3339
click.echo(file)
3440
click.echo("-" * len(file))
3541
click.echo(trace)
3642

3743

38-
@cli.command()
39-
@click.argument('crashfile', type=str)
44+
@cli.command(context_settings=CONTEXT_SETTINGS)
45+
@click.argument('crashfile', type=ExistingFilePath,)
4046
@click.option('-r', '--rerun', is_flag=True, flag_value=True,
4147
help='Rerun crashed node.')
4248
@click.option('-d', '--debug', is_flag=True, flag_value=True,
4349
help='Enable Python debugger when re-executing.')
4450
@click.option('-i', '--ipydebug', is_flag=True, flag_value=True,
4551
help='Enable IPython debugger when re-executing.')
46-
@click.option('--dir', type=str,
52+
@click.option('--dir', type=ExistingDirPath,
4753
help='Directory where to run the node in.')
4854
def crash(crashfile, rerun, debug, ipydebug, directory):
4955
"""Display Nipype crash files.
5056
5157
For certain crash files, one can rerun a failed node in a temp directory.
5258
53-
Examples:
54-
nipype crash crashfile.pklz
55-
nipype crash crashfile.pklz -r -i
56-
nipype crash crashfile.pklz -r -i
59+
Examples:\n
60+
nipype crash crashfile.pklz\n
61+
nipype crash crashfile.pklz -r -i\n
5762
"""
5863
from .crash_files import display_crash_file
5964

@@ -67,16 +72,123 @@ def crash(crashfile, rerun, debug, ipydebug, directory):
6772
display_crash_file(crashfile, rerun, debug, directory)
6873

6974

70-
@cli.command()
71-
@click.argument('pklz_file', type=str)
75+
@cli.command(context_settings=CONTEXT_SETTINGS)
76+
@click.argument('pklz_file', type=ExistingFilePath)
7277
def show(pklz_file):
7378
"""Print the content of Nipype node .pklz file.
7479
75-
Examples:
80+
Examples:\n
7681
nipype show node.pklz
7782
"""
7883
from pprint import pprint
7984
from ..utils.filemanip import loadpkl
8085

8186
pkl_data = loadpkl(pklz_file)
8287
pprint(pkl_data)
88+
89+
90+
@cli.command(context_settings=UNKNOWN_OPTIONS)
91+
@click.argument('module', type=PythonModule(), required=False)
92+
@click.argument('interface', type=str, required=False)
93+
@click.option('--list', is_flag=True, flag_value=True,
94+
help='List the available Interfaces inside the given module.')
95+
@click.option('-h', '--help', is_flag=True, flag_value=True,
96+
help='Show help message and exit.')
97+
@click.pass_context
98+
def run(ctx, module, interface, list, help):
99+
"""Run a Nipype Interface.
100+
101+
Examples:\n
102+
nipype run nipype.interfaces.nipy --list\n
103+
nipype run nipype.interfaces.nipy ComputeMask --help
104+
"""
105+
import argparse
106+
from .utils import add_args_options
107+
from ..utils.nipype_cmd import run_instance
108+
109+
# print run command help if no arguments are given
110+
module_given = bool(module)
111+
if not module_given:
112+
click.echo(ctx.command.get_help(ctx))
113+
114+
# print the list available interfaces for the given module
115+
elif (module_given and list) or (module_given and not interface):
116+
iface_names = list_interfaces(module)
117+
click.echo('Available Interfaces:')
118+
for if_name in iface_names:
119+
click.echo(' {}'.format(if_name))
120+
121+
# check the interface
122+
elif (module_given and interface):
123+
description = "Run {}".format(interface)
124+
prog = " ".join([ctx.command_path,
125+
module.__name__,
126+
interface] +
127+
ctx.args)
128+
iface_parser = argparse.ArgumentParser(description=description,
129+
prog=prog)
130+
131+
# instantiate the interface
132+
node = getattr(module, interface)()
133+
iface_parser = add_args_options(iface_parser, node)
134+
135+
if not ctx.args:
136+
# print the interface help
137+
iface_parser.print_help()
138+
else:
139+
# run the interface
140+
args = iface_parser.parse_args(args=ctx.args)
141+
run_instance(node, args)
142+
143+
144+
#
145+
# @cli.command(context_settings=CONTEXT_SETTINGS.update(dict(ignore_unknown_options=True,)))
146+
# @click.argument('-f', '--format', type=click.Choice(['boutiques']))
147+
# @click.argument('format_args', nargs=-1, type=click.UNPROCESSED)
148+
# @click.pass_context
149+
# def convert(ctx, format):
150+
# """Export nipype interfaces to other formats."""
151+
# if format == 'boutiques':
152+
# ctx.forward(to_boutiques)
153+
# import pdb; pdb.set_trace()
154+
# ctx.invoke(to_boutiques, **format_args)
155+
#
156+
#
157+
# @cli.command(context_settings=CONTEXT_SETTINGS)
158+
# @click.option("-i", "--interface", type=str, required=True,
159+
# help="Name of the Nipype interface to export.")
160+
# @click.option("-m", "--module", type=PythonModule(), required=True,
161+
# help="Module where the interface is defined.")
162+
# @click.option("-o", "--output", type=str, required=True,
163+
# help="JSON file name where the Boutiques descriptor will be written.")
164+
# @click.option("-t", "--ignored-template-inputs", type=str, multiple=True,
165+
# help="Interface inputs ignored in path template creations.")
166+
# @click.option("-d", "--docker-image", type=str,
167+
# help="Name of the Docker image where the Nipype interface is available.")
168+
# @click.option("-r", "--docker-index", type=str,
169+
# help="Docker index where the Docker image is stored (e.g. http://index.docker.io).")
170+
# @click.option("-n", "--ignore-template-numbers", is_flag=True, flag_value=True,
171+
# help="Ignore all numbers in path template creations.")
172+
# @click.option("-v", "--verbose", is_flag=True, flag_value=True,
173+
# help="Enable verbose output.")
174+
# def to_boutiques(interface, module, output, ignored_template_inputs,
175+
# docker_image, docker_index, ignore_template_numbers,
176+
# verbose):
177+
# """Nipype Boutiques exporter.
178+
#
179+
# See Boutiques specification at https://github.com/boutiques/schema.
180+
# """
181+
# from nipype.utils.nipype2boutiques import generate_boutiques_descriptor
182+
#
183+
# # Generates JSON string
184+
# json_string = generate_boutiques_descriptor(module,
185+
# interface,
186+
# ignored_template_inputs,
187+
# docker_image,
188+
# docker_index,
189+
# verbose,
190+
# ignore_template_numbers)
191+
#
192+
# # Writes JSON string to file
193+
# with open(output, 'w') as f:
194+
# f.write(json_string)

nipype/scripts/instance.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Import lib and class meta programming utilities.
4+
"""
5+
import inspect
6+
import importlib
7+
8+
from ..interfaces.base import Interface
9+
10+
11+
def import_module(module_path):
12+
"""Import any module to the global Python environment.
13+
The module_path argument specifies what module to import in
14+
absolute or relative terms (e.g. either pkg.mod or ..mod).
15+
If the name is specified in relative terms, then the package argument
16+
must be set to the name of the package which is to act as the anchor
17+
for resolving the package name (e.g. import_module('..mod', 'pkg.subpkg')
18+
19+
will import pkg.mod).
20+
21+
Parameters
22+
----------
23+
module_path: str
24+
Path to the module to be imported
25+
26+
Returns
27+
-------
28+
The specified module will be inserted into sys.modules and returned.
29+
"""
30+
try:
31+
mod = importlib.import_module(module_path)
32+
return mod
33+
except:
34+
raise ImportError('Error when importing object {}.'.format(module_path))
35+
36+
37+
def list_interfaces(module):
38+
"""Return a list with the names of the Interface subclasses inside
39+
the given module.
40+
"""
41+
iface_names = []
42+
for k, v in sorted(list(module.__dict__.items())):
43+
if inspect.isclass(v) and issubclass(v, Interface):
44+
iface_names.append(k)
45+
return iface_names
46+
47+
48+
def instantiate_this(class_path, init_args):
49+
"""Instantiates an object of the class in class_path with the given
50+
initialization arguments.
51+
52+
Parameters
53+
----------
54+
class_path: str
55+
String to the path of the class.
56+
57+
init_args: dict
58+
Dictionary of the names and values of the initialization arguments
59+
to the class
60+
61+
Return
62+
------
63+
Instantiated object
64+
"""
65+
try:
66+
cls = import_this(class_path)
67+
if init_args is None:
68+
return cls()
69+
else:
70+
return cls(**init_args)
71+
except:
72+
raise RuntimeError('Error instantiating class {} '
73+
'with the arguments {}.'.format(class_path,
74+
init_args))

nipype/scripts/utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Utilities for the CLI functions.
4+
"""
5+
from __future__ import print_function, division, unicode_literals, absolute_import
6+
import re
7+
8+
import click
9+
10+
from .instance import import_module
11+
from ..interfaces.base import InputMultiPath, traits
12+
13+
14+
# different context options
15+
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
16+
UNKNOWN_OPTIONS = dict(allow_extra_args=True,
17+
ignore_unknown_options=True)
18+
19+
20+
# specification of existing ParamTypes
21+
ExistingDirPath = click.Path(exists=True, file_okay=False, resolve_path=True)
22+
ExistingFilePath = click.Path(exists=True, dir_okay=False, resolve_path=True)
23+
24+
25+
# declare custom click.ParamType
26+
class RegularExpression(click.ParamType):
27+
name = 'regex'
28+
29+
def convert(self, value, param, ctx):
30+
try:
31+
rex = re.compile(value, re.IGNORECASE)
32+
except ValueError:
33+
self.fail('%s is not a valid regular expression.' % value, param, ctx)
34+
else:
35+
return rex
36+
37+
38+
class PythonModule(click.ParamType):
39+
name = 'Python module'
40+
41+
def convert(self, value, param, ctx):
42+
try:
43+
module = import_module(value)
44+
except ValueError:
45+
self.fail('%s is not a valid Python module.' % value, param, ctx)
46+
else:
47+
return module
48+
49+
50+
def add_args_options(arg_parser, interface):
51+
"""Add arguments to `arg_parser` to create a CLI for `interface`."""
52+
inputs = interface.input_spec()
53+
for name, spec in sorted(interface.inputs.traits(transient=None).items()):
54+
desc = "\n".join(interface._get_trait_desc(inputs, name, spec))[len(name) + 2:]
55+
args = {}
56+
57+
if spec.is_trait_type(traits.Bool):
58+
args["action"] = 'store_true'
59+
60+
if hasattr(spec, "mandatory") and spec.mandatory:
61+
if spec.is_trait_type(InputMultiPath):
62+
args["nargs"] = "+"
63+
arg_parser.add_argument(name, help=desc, **args)
64+
else:
65+
if spec.is_trait_type(InputMultiPath):
66+
args["nargs"] = "*"
67+
arg_parser.add_argument("--%s" % name, dest=name,
68+
help=desc, **args)
69+
return arg_parser

nipype/utils/nipype2boutiques.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,34 +21,6 @@
2121
import simplejson as json
2222

2323

24-
def main(argv):
25-
26-
# Parses arguments
27-
parser = argparse.ArgumentParser(description='Nipype Boutiques exporter. See Boutiques specification at https://github.com/boutiques/schema.', prog=argv[0])
28-
parser.add_argument("-i", "--interface", type=str, help="Name of the Nipype interface to export.", required=True)
29-
parser.add_argument("-m", "--module", type=str, help="Module where the interface is defined.", required=True)
30-
parser.add_argument("-o", "--output", type=str, help="JSON file name where the Boutiques descriptor will be written.", required=True)
31-
parser.add_argument("-t", "--ignored-template-inputs", type=str, help="Interface inputs ignored in path template creations.", nargs='+')
32-
parser.add_argument("-d", "--docker-image", type=str, help="Name of the Docker image where the Nipype interface is available.")
33-
parser.add_argument("-r", "--docker-index", type=str, help="Docker index where the Docker image is stored (e.g. http://index.docker.io).")
34-
parser.add_argument("-n", "--ignore-template-numbers", action='store_true', default=False, help="Ignore all numbers in path template creations.")
35-
parser.add_argument("-v", "--verbose", action='store_true', default=False, help="Enable verbose output.")
36-
37-
parsed = parser.parse_args()
38-
39-
# Generates JSON string
40-
json_string = generate_boutiques_descriptor(parsed.module,
41-
parsed.interface,
42-
parsed.ignored_template_inputs,
43-
parsed.docker_image, parsed.docker_index,
44-
parsed.verbose,
45-
parsed.ignore_template_numbers)
46-
47-
# Writes JSON string to file
48-
with open(parsed.output, 'w') as f:
49-
f.write(json_string)
50-
51-
5224
def generate_boutiques_descriptor(module, interface_name, ignored_template_inputs, docker_image, docker_index, verbose, ignore_template_numbers):
5325
'''
5426
Returns a JSON string containing a JSON Boutiques description of a Nipype interface.

0 commit comments

Comments
 (0)