|
2 | 2 | # Boutiques tools can be imported in CBRAIN (https://github.com/aces/cbrain) among other platforms.
|
3 | 3 | #
|
4 | 4 | # 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. |
8 | 12 |
|
9 | 13 | import os
|
10 | 14 | import argparse
|
|
15 | 19 |
|
16 | 20 | from nipype.interfaces.base import Interface
|
17 | 21 |
|
| 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 | + |
18 | 238 | def create_tempfile():
|
| 239 | + ''' |
| 240 | + Creates a temp file and returns its name. |
| 241 | + ''' |
19 | 242 | fileTemp = tempfile.NamedTemporaryFile(delete = False)
|
20 | 243 | fileTemp.write("hello")
|
21 | 244 | fileTemp.close()
|
22 | 245 | return fileTemp.name
|
23 | 246 |
|
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