Skip to content

Commit a30def6

Browse files
committed
nipype2boutiques: support for Boutiques spec v0.2., code cleanup, help and code documentation.
1 parent 82bff0b commit a30def6

File tree

1 file changed

+257
-93
lines changed

1 file changed

+257
-93
lines changed

nipype/utils/nipype2boutiques.py

Lines changed: 257 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
# Boutiques tools can be imported in CBRAIN (https://github.com/aces/cbrain) among other platforms.
33
#
44
# Limitations:
5-
# * optional inputs are ignored because they are not supported in Boutiques.
6-
# * inputs with cardinality "Multiple" (InputMultiPath in Nipype) are not supported. Same limitation for the outputs.
7-
# * value-templates are wrong when output files are not created in the execution directory (e.g. when a sub-directory is created).
5+
# * List outputs are not supported.
6+
# * Default values are not extracted from the documentation of the Nipype interface.
7+
# * The following input types must be ignored for the output path template creation (see option -t):
8+
# ** String restrictions, i.e. String inputs that accept only a restricted set of values.
9+
# ** mutually exclusive inputs.
10+
# * Path-templates are wrong when output files are not created in the execution directory (e.g. when a sub-directory is created).
11+
# * Optional outputs, i.e. outputs that not always produced, may not be detected.
812

913
import os
1014
import argparse
@@ -15,99 +19,259 @@
1519

1620
from nipype.interfaces.base import Interface
1721

22+
def main(argv):
23+
24+
# Parses arguments
25+
parser = argparse.ArgumentParser(description='Nipype Boutiques exporter. See Boutiques specification at https://github.com/boutiques/schema.', prog=argv[0])
26+
parser.add_argument("-i" , "--interface" , type=str, help="Name of the Nipype interface to export." , required=True)
27+
parser.add_argument("-m" , "--module" , type=str, help="Module where the interface is defined." , required=True)
28+
parser.add_argument("-o" , "--output" , type=str, help="JSON file name where the Boutiques descriptor will be written.", required=True)
29+
parser.add_argument("-t" , "--ignored-template-inputs" , type=str, help="Interface inputs ignored in path template creations.", nargs='+')
30+
parser.add_argument("-d" , "--docker-image" , type=str, help="Name of the Docker image where the Nipype interface is available.")
31+
parser.add_argument("-r" , "--docker-index" , type=str, help="Docker index where the Docker image is stored (e.g. http://index.docker.io).")
32+
parser.add_argument("-n" , "--ignore-template-numbers" , action='store_true', default=False, help="Ignore all numbers in path template creations.")
33+
parser.add_argument("-v" , "--verbose" , action='store_true', default=False, help="Enable verbose output.")
34+
35+
parsed = parser.parse_args()
36+
37+
# Generates JSON string
38+
json_string = generate_boutiques_descriptor(parsed.module,
39+
parsed.interface,
40+
parsed.ignored_template_inputs,
41+
parsed.docker_image,parsed.docker_index,
42+
parsed.verbose,
43+
parsed.ignore_template_numbers)
44+
45+
# Writes JSON string to file
46+
f = open(parsed.output,'w')
47+
f.write(json_string)
48+
f.close()
49+
50+
def generate_boutiques_descriptor(module, interface_name, ignored_template_inputs,docker_image,docker_index,verbose,ignore_template_numbers):
51+
'''
52+
Returns a JSON string containing a JSON Boutiques description of a Nipype interface.
53+
Arguments:
54+
* module: module where the Nipype interface is declared.
55+
* interface: Nipype interface.
56+
* ignored_template_inputs: a list of input names that should be ignored in the generation of output path templates.
57+
* ignore_template_numbers: True if numbers must be ignored in output path creations.
58+
'''
59+
60+
if not module:
61+
raise Exception("Undefined module.")
62+
63+
# Retrieves Nipype interface
64+
__import__(module)
65+
interface = getattr(sys.modules[module],interface_name)()
66+
inputs = interface.input_spec()
67+
outputs = interface.output_spec()
68+
69+
# Tool description
70+
tool_desc = {}
71+
tool_desc['name'] = interface_name
72+
tool_desc['command-line'] = "nipype_cmd "+str(module)+" "+interface_name+" "
73+
tool_desc['description'] = interface_name+", as implemented in Nipype (module: "+str(module)+", interface: "+interface_name+")."
74+
tool_desc['inputs'] = []
75+
tool_desc['outputs'] = []
76+
tool_desc['tool-version'] = interface.version
77+
tool_desc['schema-version'] = '0.2-snapshot'
78+
if docker_image:
79+
tool_desc['docker-image'] = docker_image
80+
if docker_index:
81+
tool_desc['docker-index'] = docker_index
82+
83+
# Generates tool inputs
84+
for name, spec in sorted(interface.inputs.traits(transient=None).items()):
85+
input = get_boutiques_input(inputs, interface, name, spec,ignored_template_inputs,verbose,ignore_template_numbers)
86+
tool_desc['inputs'].append(input)
87+
tool_desc['command-line']+= input['command-line-key']+" "
88+
if verbose:
89+
print "-> Adding input "+input['name']
90+
91+
# Generates tool outputs
92+
for name,spec in sorted(outputs.traits(transient=None).items()):
93+
output = get_boutiques_output(name,interface,tool_desc['inputs'],verbose)
94+
if output['path-template'] != "":
95+
tool_desc['outputs'].append(output)
96+
if verbose:
97+
print "-> Adding output "+output['name']
98+
elif verbose:
99+
print "xx Skipping output "+output['name']+" with no path template."
100+
if tool_desc['outputs'] == []:
101+
raise Exception("Tool has no output.")
102+
103+
# Removes all temporary values from inputs (otherwise they will
104+
# appear in the JSON output)
105+
for input in tool_desc['inputs']:
106+
del input['tempvalue']
107+
108+
return json.dumps(tool_desc, indent=4, separators=(',', ': '))
109+
110+
def get_boutiques_input(inputs,interface,input_name,spec,ignored_template_inputs,verbose,ignore_template_numbers):
111+
"""
112+
Returns a dictionary containing the Boutiques input corresponding to a Nipype intput.
113+
114+
Args:
115+
* inputs: inputs of the Nipype interface.
116+
* interface: Nipype interface.
117+
* input_name: name of the Nipype input.
118+
* spec: Nipype input spec.
119+
* ignored_template_inputs: input names for which no temporary value must be generated.
120+
* ignore_template_numbers: True if numbers must be ignored in output path creations.
121+
122+
Assumes that:
123+
* Input names are unique.
124+
"""
125+
if not spec.desc:
126+
spec.desc = "No description provided."
127+
spec_info = spec.full_info(inputs, input_name, None)
128+
129+
input = {}
130+
input['id'] = input_name
131+
input['name'] = input_name.replace('_',' ').capitalize()
132+
input['type'] = get_type_from_spec_info(spec_info)
133+
input['list'] = is_list(spec_info)
134+
input['command-line-key'] = "["+input_name.upper()+"]" # assumes that input names are unique
135+
input['command-line-flag'] = ("--%s"%input_name+" ").strip()
136+
input['tempvalue'] = None
137+
input['description'] = spec_info.capitalize()+". "+spec.desc.capitalize()
138+
if not input['description'].endswith('.'):
139+
input['description'] += '.'
140+
if not ( hasattr(spec, "mandatory") and spec.mandatory ):
141+
input['optional'] = True
142+
else:
143+
input['optional'] = False
144+
if spec.usedefault:
145+
input['default-value'] = spec.default_value()[1]
146+
147+
148+
# Create unique, temporary value.
149+
temp_value = must_generate_value(input_name,input['type'],ignored_template_inputs,spec_info,spec,ignore_template_numbers)
150+
if temp_value:
151+
tempvalue = get_unique_value(input['type'],input_name)
152+
setattr(interface.inputs,input_name,tempvalue)
153+
input['tempvalue'] = tempvalue
154+
if verbose:
155+
print "oo Path-template creation using "+input['id']+"="+str(tempvalue)
156+
157+
# Now that temp values have been generated, set Boolean types to
158+
# Number (there is no Boolean type in Boutiques)
159+
if input['type'] == "Boolean":
160+
input['type'] = "Number"
161+
162+
return input
163+
164+
def get_boutiques_output(name,interface,tool_inputs,verbose=False):
165+
"""
166+
Returns a dictionary containing the Boutiques output corresponding to a Nipype output.
167+
168+
Args:
169+
* name: name of the Nipype output.
170+
* interface: Nipype interface.
171+
* tool_inputs: list of tool inputs (as produced by method get_boutiques_input).
172+
173+
Assumes that:
174+
* Output names are unique.
175+
* Input values involved in the path template are defined.
176+
* Output files are written in the current directory.
177+
* There is a single output value (output lists are not supported).
178+
"""
179+
output = {}
180+
output['name'] = name.replace('_',' ').capitalize()
181+
output['id'] = name
182+
output['type'] = "File"
183+
output['path-template'] = ""
184+
output['optional'] = True # no real way to determine if an output is always produced, regardless of the input values.
185+
186+
# Path template creation.
187+
188+
output_value = interface._list_outputs()[name]
189+
if output_value != "" and isinstance(output_value,str): # FIXME: this crashes when there are multiple output values.
190+
# Go find from which input value it was built
191+
for input in tool_inputs:
192+
if not input['tempvalue']:
193+
continue
194+
input_value = input['tempvalue']
195+
if input['type'] == "File":
196+
# Take the base name
197+
input_value = os.path.splitext(os.path.basename(input_value))[0]
198+
if str(input_value) in output_value:
199+
output_value = os.path.basename(output_value.replace(input_value,input['command-line-key'])) # FIXME: this only works if output is written in the current directory
200+
output['path-template'] = os.path.basename(output_value)
201+
return output
202+
203+
def get_type_from_spec_info(spec_info):
204+
'''
205+
Returns an input type from the spec info. There must be a better
206+
way to get an input type in Nipype than to parse the spec info.
207+
'''
208+
if ("an existing file name" in spec_info) or ("input volumes" in spec_info):
209+
return "File"
210+
elif ("an integer" in spec_info or "a float" in spec_info) :
211+
return "Number"
212+
elif "a boolean" in spec_info:
213+
return "Boolean"
214+
return "String"
215+
216+
def is_list(spec_info):
217+
'''
218+
Returns True if the spec info looks like it describes a list
219+
parameter. There must be a better way in Nipype to check if an input
220+
is a list.
221+
'''
222+
if "a list" in spec_info:
223+
return True
224+
return False
225+
226+
def get_unique_value(type,id):
227+
'''
228+
Returns a unique value of type 'type', for input with id 'id',
229+
assuming id is unique.
230+
'''
231+
return {
232+
"File": os.path.abspath(create_tempfile()),
233+
"Boolean": True,
234+
"Number": abs(hash(id)), # abs in case input param must be positive...
235+
"String": id
236+
}[type]
237+
18238
def create_tempfile():
239+
'''
240+
Creates a temp file and returns its name.
241+
'''
19242
fileTemp = tempfile.NamedTemporaryFile(delete = False)
20243
fileTemp.write("hello")
21244
fileTemp.close()
22245
return fileTemp.name
23246

24-
def print_inputs(tool_name, module=None, function=None):
25-
interface = None
26-
if module and function:
27-
__import__(module)
28-
interface = getattr(sys.modules[module],function)()
29-
30-
inputs = interface.input_spec()
31-
outputs = interface.output_spec()
32-
33-
command_line = "nipype_cmd "+str(module)+" "+tool_name+" "
34-
tool_desc = {}
35-
tool_desc['name'] = tool_name
36-
tool_desc['description'] = "Tool description goes here"
37-
38-
tool_inputs = []
39-
input_counter = 0
40-
tool_outputs = []
41-
42-
for name, spec in sorted(interface.inputs.traits(transient=None).items()):
43-
44-
input = {}
45-
46-
input['name'] = name
47-
type = spec.full_info(inputs, name, None)
48-
if "an existing file name" in type:
49-
type = "File"
50-
else:
51-
type = "String"
52-
input['type'] = type
53-
input['description'] = "\n".join(interface._get_trait_desc(inputs, name, spec))[len(name)+2:].replace("\n\t\t",". ")
54-
command_line_key = "["+str(input_counter)+"_"+name.upper()+"]"
55-
input_counter += 1
56-
input['command-line-key'] = command_line_key
57-
input['cardinality'] = "Single"
58-
if not ( hasattr(spec, "mandatory") and spec.mandatory ):
59-
input['optional'] = "true"
60-
input['command-line-flag'] = "--%s"%name+" "
61-
62-
tool_inputs.append(input)
63-
64-
command_line+= command_line_key+" "
65-
66-
# add value to input so that output names can be generated
67-
tempfile_name = create_tempfile()
68-
input['tempfile_name'] = tempfile_name
69-
if type == "File":
70-
setattr(interface.inputs,name,os.path.abspath(tempfile_name))
71-
72-
for name,spec in sorted(outputs.traits(transient=None).items()):
73-
74-
output = {}
75-
output['name'] = name
76-
output['type'] = "File"
77-
output['description'] = "No description provided"
78-
output['command-line-key'] = ""
79-
output['value-template'] = ""
80-
output_value = interface._list_outputs()[name]
81-
if output_value != "" and isinstance(output_value,str): # FIXME: this crashes when there are multiple output values.
82-
# go find from which input file it was built
83-
for input in tool_inputs:
84-
base_file_name = os.path.splitext(os.path.basename(input['tempfile_name']))[0]
85-
if base_file_name in output_value:
86-
output_value = os.path.basename(output_value.replace(base_file_name,input['command-line-key'])) # FIXME: this only works if output is written in the current directory
87-
output['value-template'] = os.path.basename(output_value)
88-
89-
output['cardinality'] = "Single"
90-
tool_outputs.append(output)
91-
92-
# remove all temporary file names from inputs
93-
for input in tool_inputs:
94-
del input['tempfile_name']
95-
96-
tool_desc['inputs'] = tool_inputs
97-
tool_desc['outputs'] = tool_outputs
98-
tool_desc['command-line'] = command_line
99-
tool_desc['docker-image'] = 'docker.io/robdimsdale/nipype'
100-
tool_desc['docker-index'] = 'http://index.docker.io'
101-
tool_desc['schema-version'] = '0.2-snapshot'
102-
print json.dumps(tool_desc, indent=4, separators=(',', ': '))
103-
104-
def main(argv):
105-
106-
parser = argparse.ArgumentParser(description='Nipype Boutiques exporter', prog=argv[0])
107-
parser.add_argument("module", type=str, help="Module name")
108-
parser.add_argument("interface", type=str, help="Interface name")
109-
parsed = parser.parse_args(args=argv[1:3])
110-
111-
_, prog = os.path.split(argv[0])
112-
interface_parser = argparse.ArgumentParser(description="Run %s"%parsed.interface, prog=" ".join([prog] + argv[1:3]))
113-
print_inputs(argv[2],parsed.module, parsed.interface)
247+
def must_generate_value(name,type,ignored_template_inputs,spec_info,spec,ignore_template_numbers):
248+
'''
249+
Return True if a temporary value must be generated for this input.
250+
Arguments:
251+
* name: input name.
252+
* type: input_type.
253+
* ignored_template_inputs: a list of inputs names for which no value must be generated.
254+
* spec_info: spec info of the Nipype input
255+
. * ignore_template_numbers: True if numbers must be ignored.
256+
'''
257+
# Return false when type is number and numbers must be ignored.
258+
if ignore_template_numbers and type == "Number":
259+
return False
260+
# Only generate value for the first element of mutually exclusive inputs.
261+
if spec.xor and spec.xor[0]!=name:
262+
return False
263+
# Directory types are not supported
264+
if "an existing directory name" in spec_info:
265+
return False
266+
# Don't know how to generate a list.
267+
if "a list" in spec_info or "a tuple" in spec_info:
268+
return False
269+
# Don't know how to generate a dictionary.
270+
if "a dictionary" in spec_info:
271+
return False
272+
# Best guess to detect string restrictions...
273+
if "' or '" in spec_info:
274+
return False
275+
if not ignored_template_inputs:
276+
return True
277+
return not (name in ignored_template_inputs)

0 commit comments

Comments
 (0)