2525
2626
2727def generate_boutiques_descriptor (
28- module , interface_name , ignored_template_inputs , container_image ,
29- container_index , container_type , verbose , ignore_template_numbers , save ):
28+ module , interface_name , container_image , container_type , container_index = None ,
29+ ignored_template_inputs = (), ignore_template_numbers = False , verbose = False , save = False ):
3030 '''
3131 Returns a JSON string containing a JSON Boutiques description of a Nipype interface.
3232 Arguments:
3333 * module: module where the Nipype interface is declared.
3434 * interface_name: name of Nipype interface.
35- * ignored_template_inputs: a list of input names that should be ignored in the generation of output path templates.
36- * ignore_template_numbers: True if numbers must be ignored in output path creations.
3735 * container_image: name of the container image where the tool is installed
38- * container_index: optional index where the image is available
3936 * container_type: type of container image (Docker or Singularity)
37+ * container_index: optional index where the image is available
38+ * ignored_template_inputs: a list of input names that should be ignored in the generation of output path templates.
39+ * ignore_template_numbers: True if numbers must be ignored in output path creations.
40+ * verbose: print information messages
4041 * save: True if you want to save descriptor to a file
4142 '''
4243
@@ -105,23 +106,21 @@ def generate_boutiques_descriptor(
105106 del tool_desc ['groups' ]
106107
107108 # Generates tool outputs
108- for name , spec in sorted (outputs .traits (transient = None ).items ()):
109- output = get_boutiques_output (outputs , name , spec , interface , tool_desc ['inputs' ],
110- verbose )
111- if output ['path-template' ] != "" :
112- tool_desc ['output-files' ].append (output )
113- if verbose :
114- print ("-> Adding output " + output ['name' ])
115- elif verbose :
116- print ("xx Skipping output " + output ['name' ] +
117- " with no path template." )
118- if tool_desc ['output-files' ] == []:
119- raise Exception ("Tool has no output." )
109+ generate_tool_outputs (outputs , interface , tool_desc , verbose , True )
120110
121- # Removes all temporary values from inputs (otherwise they will
122- # appear in the JSON output)
123- for input in tool_desc ['inputs' ]:
124- del input ['tempvalue' ]
111+ # Generate outputs with various different inputs to try to generate
112+ # as many output values as possible
113+ custom_inputs = generate_custom_inputs (tool_desc ['inputs' ])
114+
115+ for input_dict in custom_inputs :
116+ interface = getattr (module , interface_name )(** input_dict )
117+ outputs = interface .output_spec ()
118+ generate_tool_outputs (outputs , interface , tool_desc , verbose , False )
119+
120+ # Fill in all missing output paths
121+ for output in tool_desc ['output-files' ]:
122+ if output ['path-template' ] == "" :
123+ fill_in_missing_output_path (output , output ['name' ], tool_desc ['inputs' ])
125124
126125 # Save descriptor to a file
127126 if save :
@@ -135,6 +134,26 @@ def generate_boutiques_descriptor(
135134 return json .dumps (tool_desc , indent = 4 , separators = (',' , ': ' ))
136135
137136
137+ def generate_tool_outputs (outputs , interface , tool_desc , verbose , first_run ):
138+ for name , spec in sorted (outputs .traits (transient = None ).items ()):
139+ output = get_boutiques_output (outputs , name , spec , interface , tool_desc ['inputs' ],
140+ verbose )
141+ # If this is the first time we are generating outputs, add the full output to the descriptor.
142+ # Otherwise, find the existing output and update its path template if it's still undefined.
143+ if first_run :
144+ tool_desc ['output-files' ].append (output )
145+ else :
146+ for existing_output in tool_desc ['output-files' ]:
147+ if output ['id' ] == existing_output ['id' ] and existing_output ['path-template' ] == "" :
148+ existing_output ['path-template' ] = output ['path-template' ]
149+ break
150+ if verbose :
151+ print ("-> Adding output " + output ['name' ])
152+
153+ if len (tool_desc ['output-files' ]) == 0 :
154+ raise Exception ("Tool has no output." )
155+
156+
138157def get_boutiques_input (inputs , interface , input_name , spec ,
139158 ignored_template_inputs , verbose ,
140159 ignore_template_numbers , handler = None ,
@@ -155,7 +174,6 @@ def get_boutiques_input(inputs, interface, input_name, spec,
155174 Assumes that:
156175 * Input names are unique.
157176 """
158- spec_info = spec .full_info (inputs , input_name , None )
159177
160178 input = {}
161179
@@ -234,7 +252,6 @@ def get_boutiques_input(inputs, interface, input_name, spec,
234252 elif input ['type' ] == "Flag" :
235253 input ['command-line-flag' ] = ("--%s" % input_name + " " ).strip ()
236254
237- input ['tempvalue' ] = None
238255 input ['description' ] = get_description_from_spec (inputs , input_name , spec )
239256 if not (hasattr (spec , "mandatory" ) and spec .mandatory ):
240257 input ['optional' ] = True
@@ -251,20 +268,7 @@ def get_boutiques_input(inputs, interface, input_name, spec,
251268 if value_choices is not None :
252269 input ['value-choices' ] = value_choices
253270
254- # Create unique, temporary value.
255- temp_value = must_generate_value (input_name , input ['type' ],
256- ignored_template_inputs , spec_info , spec ,
257- ignore_template_numbers )
258- if temp_value :
259- tempvalue = get_unique_value (input ['type' ], input_name )
260- setattr (interface .inputs , input_name , tempvalue )
261- input ['tempvalue' ] = tempvalue
262- if verbose :
263- print ("oo Path-template creation using " + input ['id' ] + "=" +
264- str (tempvalue ))
265-
266- # Now that temp values have been generated, set Boolean types to
267- # Flag (there is no Boolean type in Boutiques)
271+ # Set Boolean types to Flag (there is no Boolean type in Boutiques)
268272 if input ['type' ] == "Boolean" :
269273 input ['type' ] = "Flag"
270274
@@ -313,38 +317,13 @@ def get_boutiques_output(outputs, name, spec, interface, tool_inputs, verbose=Fa
313317 except TypeError :
314318 output_value = None
315319
316- # If output value is defined, use its basename
317- if output_value != "" and isinstance (
318- output_value ,
319- str ): # FIXME: this crashes when there are multiple output values.
320- # Go find from which input value it was built
321- for input in tool_inputs :
322- if not input ['tempvalue' ]:
323- continue
324- input_value = input ['tempvalue' ]
325- if input ['type' ] == "File" :
326- # Take the base name
327- input_value = os .path .splitext (
328- os .path .basename (input_value ))[0 ]
329- if str (input_value ) in output_value :
330- output_value = os .path .basename (
331- output_value .replace (input_value ,
332- input ['value-key' ])
333- ) # FIXME: this only works if output is written in the current directory
334- output ['path-template' ] = os .path .basename (output_value )
335-
336- # If output value is undefined, create a placeholder for the path template
337- if not output_value :
338- # Look for an input with the same name and use this as the path template
339- found = False
340- for input in tool_inputs :
341- if input ['id' ] == name :
342- output ['path-template' ] = input ['value-key' ]
343- found = True
344- break
345- # If no input with the same name was found, use the output ID
346- if not found :
347- output ['path-template' ] = output ['id' ]
320+ # If an output value is defined, use its relative path
321+ # Otherwise, put blank string and try to fill it on another iteration
322+ if output_value :
323+ output ['path-template' ] = os .path .relpath (output_value )
324+ else :
325+ output ['path-template' ] = ""
326+
348327 return output
349328
350329
@@ -379,6 +358,7 @@ def get_boutiques_groups(input_traits):
379358
380359 return desc_groups
381360
361+
382362def get_unique_value (type , id ):
383363 '''
384364 Returns a unique value of type 'type', for input with id 'id',
@@ -453,3 +433,63 @@ def get_description_from_spec(object, name, spec):
453433 boutiques_description += '.'
454434
455435 return boutiques_description
436+
437+
438+ def fill_in_missing_output_path (output , output_name , tool_inputs ):
439+ '''
440+ Creates a path template for outputs that are missing one
441+ This is needed for the descriptor to be valid (path template is required)
442+ '''
443+ # Look for an input with the same name as the output and use its value key
444+ found = False
445+ for input in tool_inputs :
446+ if input ['name' ] == output_name :
447+ output ['path-template' ] = input ['value-key' ]
448+ found = True
449+ break
450+ # If no input with the same name was found, use the output ID
451+ if not found :
452+ output ['path-template' ] = output ['id' ]
453+ return output
454+
455+
456+
457+ def generate_custom_inputs (desc_inputs ):
458+ '''
459+ Generates a bunch of custom input dictionaries in order to generate as many outputs as possible
460+ (to get their path templates)
461+ Limitations:
462+ -Does not support String inputs since some interfaces require specific strings
463+ -Does not support File inputs since the file has to actually exist or the interface will fail
464+ -Does not support list inputs yet
465+ '''
466+ custom_input_dicts = []
467+ for desc_input in desc_inputs :
468+ if desc_input .get ('list' ): # TODO support list inputs
469+ continue
470+ if desc_input ['type' ] == 'Flag' :
471+ custom_input_dicts .append ({desc_input ['id' ]: True })
472+ elif desc_input ['type' ] == 'Number' :
473+ custom_input_dicts .append ({desc_input ['id' ]: generate_random_number_input (desc_input )})
474+ elif desc_input .get ('value-choices' ):
475+ for value in desc_input ['value-choices' ]:
476+ custom_input_dicts .append ({desc_input ['id' ]: value })
477+ return custom_input_dicts
478+
479+
480+ def generate_random_number_input (desc_input ):
481+ '''
482+ Generates a random number input based on the input spec
483+ '''
484+ if not desc_input .get ('minimum' ) and not desc_input .get ('maximum' ):
485+ return 1
486+
487+ if desc_input .get ('integer' ):
488+ offset = 1
489+ else :
490+ offset = 0.1
491+
492+ if desc_input .get ('minimum' ):
493+ return desc_input ['minimum' ] if desc_input .get ('exclusive-minimum' ) else desc_input ['minimum' ] + offset
494+ if desc_input .get ('maximum' ):
495+ return desc_input ['maximum' ] if desc_input .get ('exclusive-maximum' ) else desc_input ['maximum' ] - offset
0 commit comments