def returntype(self): lhs_val: DataType = self.args[0] rhs_val: DataType = self.args[1] if isinstance(lhs_val, Selector): lhs = get_instantiated_type(lhs_val.returntype()) else: lhs = get_instantiated_type(lhs_val) if isinstance(rhs_val, Selector): rhs = get_instantiated_type(rhs_val.returntype()) else: rhs = get_instantiated_type(rhs_val) if isinstance(lhs, (String, File, Directory)) or isinstance( rhs, (String, File, Directory)): return String() if isinstance(lhs, Float) or isinstance(rhs, Float): return Double() if isinstance(lhs, Float) or isinstance(rhs, Float): return Float() if isinstance(lhs, Int) and isinstance(rhs, Int): return Int() raise TypeError( f"Unsure how to derive returntype from {lhs.id()} + {rhs.id()}")
def find_connection( self, source_dt: ParseableType, desired_dt: ParseableType) -> List[JanisTransformation]: from inspect import getmro source = get_instantiated_type(source_dt) desired = get_instantiated_type(desired_dt) if desired.can_receive_from(source): return [] types = getmro(type(source)) for T in types: if not issubclass(T, DataType) or T == DataType: continue transformation = self.find_connection_inner(T, desired) if transformation is not None: return transformation raise Exception( f"There's no transformation that can satisfy {source.name()} -> {desired.name()}" )
def returntype(self): args = [] for a in self.args[1:]: if isinstance(a, Selector): args.append(get_instantiated_type(a.returntype())) else: args.append(get_instantiated_type(a)) return UnionType(*args)
def add_source(self, operator: Selector, should_scatter) -> Edge: """ Add a connection :param start: :param stag: :param should_scatter: :return: """ from janis_core.workflow.workflow import StepNode # start: Node, stag: Optional[str] # stype = (start.outputs()[stag] if stag is not None else first_value(start.outputs())).outtype stype = get_instantiated_type(operator.returntype()) ftype = (self.finish.inputs()[self.ftag] if self.ftag is not None else first_value(self.finish.inputs())).intype # start_is_scattered = isinstance(start, StepNode) and start.scatter is not None # # if start_is_scattered: # Logger.log( # f"This edge merges the inputs from '{full_dot(start, stag)}' for " # f"'{full_dot(self.finish, self.ftag)}'" # ) # stype = Array(stype) if should_scatter: if not stype.is_array(): raise Exception( f"Scatter was required for '{operator} → '{self.finish.id()}.{self.ftag}' but " f"the input type was {type(stype).__name__} and not an array" ) stype = get_instantiated_type(stype.subtype()) if len(self.source_map) == 1: # and start.id() not in self.source_map: self.multiple_inputs = True if not ftype.is_array(): raise Exception( f"Adding multiple inputs to '{self.finish.id()}' and '{ftype.id()}' is not an array" ) if not stype.is_array() and ftype.is_array(): # https://www.commonwl.org/user_guide/misc/#connect-a-solo-value-to-an-input-that-expects-an-array-of-that-type self.multiple_inputs = True e = Edge(operator, self.finish, self.ftag, should_scatter=should_scatter) # todo: deal with source_map self.source_map.append(e) return e
def returntype(self): from copy import copy a = self.args[0] if isinstance(a, Selector): ret = get_instantiated_type(a.returntype()) else: ret = get_instantiated_type(a) ret = copy(ret) ret.optional = False return ret
def output( self, identifier: str, datatype: Optional[ParseableType] = None, source: Union[StepNode, ConnectionSource] = None, output_tag: Union[ str, InputSelector, ConnectionSource, List[Union[str, InputSelector, ConnectionSource]], ] = None, output_prefix: Union[str, InputSelector, ConnectionSource] = None, ): """ Create an output on a workflow :param identifier: The identifier for the output :param datatype: Optional data type of the output to check. This will be automatically inferred if not provided. :param source: The source of the output, must be an output to a step node :param output_tag: A janis annotation for grouping outputs by this value. If a list is passed, it represents a structure of nested directories, the first element being the root directory. At most, one InputSelector can resolve to an array, and this behaviour is only defined if the output scattered source, and the number of elements is equal. :param output_prefix: Decides the prefix that an output will have, or acts as a map if the InputSelector resolves to an array with equal length to the number of shards (scatters). Any other behaviour is defined and may result in an unexpected termination. :return: """ self.verify_identifier(identifier, repr(datatype)) if source is None: raise Exception("Output source must not be 'None'") node, tag = verify_or_try_get_source(source) if not datatype: datatype: DataType = node.outputs()[tag].output_type.received_type() if isinstance(node, StepNode) and node.scatter: datatype = Array(datatype) if output_prefix: op = output_prefix if isinstance(output_prefix, list) else [output_prefix] output_prefix = self.verify_output_source_type( identifier, op, "output_prefix" ) if output_tag: ot = output_tag if isinstance(output_tag, list) else [output_tag] output_tag = self.verify_output_source_type(identifier, ot, "output_tag") otp = OutputNode( self, identifier=identifier, datatype=get_instantiated_type(datatype), source=(node, tag), output_tag=output_tag, output_prefix=output_prefix, ) self.nodes[identifier] = otp self.output_nodes[identifier] = otp return otp
def input( self, identifier: str, datatype: ParseableType, default: any = None, value: any = None, doc: Union[str, InputDocumentation] = None, ): """ Create an input node on a workflow :return: """ self.verify_identifier(identifier, repr(datatype)) datatype = get_instantiated_type(datatype) if default is not None: datatype.optional = True doc = (doc if isinstance(doc, InputDocumentation) else InputDocumentation(doc=doc)) inp = InputNode( self, identifier=identifier, datatype=datatype, default=default, doc=doc, value=value, ) self.nodes[identifier] = inp self.input_nodes[identifier] = inp return inp
def __init__( self, input_to_select, remove_file_extension=None, type_hint=File, **kwargs ): """ :param input_to_select: The name of the input to select :param remove_file_extension: Call basename() and remove the file extension :param type_hint: Janis can't determine the type of the input to select until translation time, so providing a hint type might suppress false warnings. This is similar to using .as_type(dt) """ if not isinstance(input_to_select, str): raise Exception( f"Expected input_to_select to be string, not {type(input_to_select)}: {str(input_to_select)}" ) # maybe worth validating the input_to_select identifier self.input_to_select = input_to_select self.type_hint = get_instantiated_type(type_hint) or File() if "use_basename" in kwargs: use_basename = kwargs["use_basename"] if remove_file_extension is None: remove_file_extension = use_basename Logger.warn( f"The 'use_basename' key is deprecated, please use 'remove_file_extension' instead: " f'InputSelector("{self.input_to_select}", remove_file_extension={str(use_basename)})' ) self.remove_file_extension = remove_file_extension
def __init__( self, tag: str, output_type: ParseableType, glob: Optional[Union[Selector, str]] = None, doc: Optional[str] = None, ): """ A ToolOutput instructs the the engine how to collect an output and how it may be referenced in a workflow. :param tag: The identifier of a output, must be unique in the inputs and outputs. :param output_type: The type of output that is being collected. :param glob: How to collect this output, can accept any :class:`janis.Selector`. :param doc: Documentation on what the output is, used to generate docs. """ if not Validators.validate_identifier(tag): raise Exception( f"The identifier '{tag}' was invalid because {Validators.reason_for_failure(tag)}" ) self.tag = tag self.output_type: ParseableType = get_instantiated_type(output_type) self.glob = glob self.doc = doc
def input( self, identifier: str, datatype: ParseableType, default: any = None, value: any = None, doc: str = None, ): """ Create an input node on a workflow :return: """ self.verify_identifier(identifier, repr(datatype)) datatype = get_instantiated_type(datatype) if default: datatype.optional = True inp = InputNode( self, identifier=identifier, datatype=datatype, default=default, doc=doc, value=value, ) self.nodes[identifier] = inp self.input_nodes[identifier] = inp return inp
def inputs(self): if self._cached_input_signature is None: import inspect argspec = inspect.signature(self.code_block) docstrings = parse_docstring(self.code_block.__doc__) paramlist = docstrings.get("params", []) paramdocs = { p["name"]: p.get("doc").strip() for p in paramlist if "name" in p } missing_annotations = set() unsupported_types = {} ins = [] for inp in argspec.parameters.values(): if inp.name in inspect_ignore_keys: continue fdefault = inp.default optional = (fdefault is not inspect.Parameter.empty) or fdefault is None default = fdefault if optional else None defaulttype = type(fdefault) if fdefault is not None else None annotation = (defaulttype if inp.annotation is inspect.Parameter.empty else inp.annotation) if not annotation: missing_annotations.add(inp.name) continue dt_type: Optional[DataType] = get_instantiated_type( annotation, optional=optional) if not dt_type: unsupported_types[inp.name] = annotation continue ins.append( TInput( tag=inp.name, intype=dt_type, default=default, doc=InputDocumentation(paramdocs.get(inp.name)), )) if missing_annotations: raise Exception( f"The following types on the PythonTool '{self.id()}' were missing type annotations (REQUIRED): " + ", ".join(missing_annotations)) if unsupported_types: raise Exception(f"Unsupported types for inputs: " + ", ".join( f"{k}: {v}" for k, v in unsupported_types.items())) self._cached_input_signature = ins return self._cached_input_signature
def __init__( self, tag: str, output_type: ParseableType, glob: Optional[Union[Selector, str]] = None, presents_as: str = None, secondaries_present_as: Dict[str, str] = None, doc: Optional[Union[str, OutputDocumentation]] = None, ): """ A ToolOutput instructs the the engine how to collect an output and how it may be referenced in a workflow. :param tag: The identifier of a output, must be unique in the inputs and outputs. :param output_type: The type of output that is being collected. :param glob: How to collect this output, can accept any :class:`janis.Selector`. :param doc: Documentation on what the output is, used to generate docs. """ if not Validators.validate_identifier(tag): raise Exception( f"The identifier '{tag}' was invalid because {Validators.reason_for_failure(tag)}" ) self.tag = tag self.output_type: ParseableType = get_instantiated_type(output_type) if not glob and not ( isinstance(self.output_type, Stdout) or isinstance(self.output_type, Stderr) ): raise Exception( "ToolOutput expects a glob when the output type is not Stdout / Stderr" ) self.glob = glob self.presents_as = presents_as self.secondaries_present_as = secondaries_present_as self.doc = ( doc if isinstance(doc, OutputDocumentation) else OutputDocumentation(doc=doc) ) if self.secondaries_present_as: if not self.output_type.secondary_files(): raise Exception( f"The ToolOutput '{self.id()}' requested a rewrite of secondary file extension through " f"'secondaries_present_as', but the type {self.output_type.id()} not have any secondary files." ) secs = set(self.output_type.secondary_files()) to_remap = set(self.secondaries_present_as.keys()) invalid = to_remap - secs if len(invalid) > 0: raise Exception( f"Error when constructing output '{self.id()}', the secondaries_present_as contained secondary " f"files ({', '.join(invalid)}) that were not found in the output " f"type '{self.output_type.id()}' ({', '.join(secs)})" )
def __init__(self, tag: str, intype: DataType, default=None, doc: InputDocumentation = None): self.tag = tag self.intype = get_instantiated_type(intype) self.default = default self.doc = doc
def to_wdl(self, unwrap_operator, *args): iterable, separator = [unwrap_operator(a) for a in self.args] iterable_arg = self.args[0] if isinstance(iterable_arg, list): is_optional = any( get_instantiated_type(a.returntype()).optional for a in iterable_arg ) else: rettype = get_instantiated_type(iterable_arg.returntype()) if rettype.is_array(): is_optional = rettype.subtype().optional else: is_optional = rettype.optional if is_optional: return f"sep({separator}, select_first([{iterable}, []]))" else: return f"sep({separator}, {iterable})"
def returntype(self): if isinstance(self.args[0], list): rettype = self.args[0][0].returntype() else: outer_rettype = get_instantiated_type(self.args[0].returntype()) if not isinstance(outer_rettype, Array): # hmmm, this could be a bad input selector rettype = outer_rettype if not isinstance(self.args[0], InputSelector): Logger.warn( f'Expected return type of "{self.args[0]}" to be an array, ' f'but found {outer_rettype}, will return this as a returntype.' ) else: rettype = outer_rettype.subtype() rettype = copy(get_instantiated_type(rettype)) rettype.optional = False return Array(rettype)
def __init__( self, tag: str, input_type: ParseableType, position: Optional[int] = None, prefix: Optional[str] = None, separate_value_from_prefix: bool = None, prefix_applies_to_all_elements: bool = None, separator: str = None, shell_quote: bool = None, localise_file: bool = None, default: Any = None, doc: Optional[str] = None, ): """ A ``ToolInput`` represents an input to a tool, with parameters that allow it to be bound on the command line. The ToolInput must have either a position or prefix set to be bound onto the command line. :param tag: The identifier of the input (unique to inputs and outputs of a tool) :param input_type: The data type that this input accepts :type input_type: ``janis.ParseableType`` :param position: The position of the input to be applied. (Default = 0, after the base_command). :param prefix: The prefix to be appended before the element. (By default, a space will also be applied, see ``separate_value_from_prefix`` for more information) :param separate_value_from_prefix: (Default: True) Add a space between the prefix and value when ``True``. :param prefix_applies_to_all_elements: Applies the prefix to each element of the array (Array inputs only) :param shell_quote: Stops shell quotes from being applied in all circumstances, useful when joining multiple commands together. :param separator: The separator between each element of an array (defaults to ' ') :param localise_file: Ensures that the file(s) are localised into the execution directory. :param default: The default value to be applied if the input is not defined. :param doc: Documentation string for the ToolInput, this is used to generate the tool documentation and provide hints to the user. """ super().__init__( value=None, prefix=prefix, position=position, separate_value_from_prefix=separate_value_from_prefix, doc=doc, shell_quote=shell_quote, ) # if default is not None: # input_type.optional = True if not Validators.validate_identifier(tag): raise Exception( f"The identifier '{tag}' was not validated because {Validators.reason_for_failure(tag)}" ) self.tag: str = tag self.input_type: ParseableType = get_instantiated_type(input_type) self.default = default self.prefix_applies_to_all_elements = prefix_applies_to_all_elements self.separator = separator self.localise_file = localise_file
def basename(self): from .standard import BasenameOperator outtype = get_instantiated_type(self.returntype()).received_type() if not isinstance(outtype, (File, Directory)): raise Exception( "Basename operator can only be applied to steps of File / Directory type, received: " + str(outtype) ) return BasenameOperator(self)
def __init__( self, start_type: ParseableType, finish_type: ParseableType, tool: Tool, relevant_tool_input: Optional[str] = None, relevant_tool_output: Optional[str] = None, ): self.type1 = get_instantiated_type(start_type) self.type2 = get_instantiated_type(finish_type) self.tool = tool connection_type = f"`{self.type1} -> {self.type2}`" Logger.log( f"Building transformation for {connection_type} using tool '{tool.id()}" ) self.relevant_tool_input = self.evaluate_tool_input( relevant_tool_input) self.relevant_tool_output = self.evaluate_tool_output( relevant_tool_output)
def validate(self, perform_typecheck=False): args = self.args argtypes = self.argtypes() if len(args) < len(argtypes): # missing params nmissing = len(argtypes) - len(args) raise TypeError( f"{self.__class__.__name__} missing {nmissing} required positional argument" ) elif len(args) > len(argtypes): raise TypeError( f"{self.__class__.__name__} expects {len(argtypes)} arguments, but {len(args)} were given" ) errors = [] if perform_typecheck: for i in range(len(args)): expected = get_instantiated_type(argtypes[i]) received_arg = args[i] if isinstance(received_arg, Selector): received = get_instantiated_type(args[i].returntype()) else: received = get_instantiated_type(received_arg) if not expected.can_receive_from(received): errors.append( f"argument {i+1} '{received.id()}' was incompatible with the expected {expected.id()}" ) if errors: singularprefix = "were errors" if len( errors) != 1 else "was an error" raise TypeError( f"There {singularprefix} in the arguments when validating '{self.__class__.__name__}', " + ", ".join(errors)) return True
def check_types(self): from janis_core.workflow.workflow import InputNode, StepNode # stoolin: TOutput = self.start.outputs()[ # self.stag # ] if self.stag is not None else first_value(self.start.outputs()) ftoolin: TInput = (self.finish.inputs()[self.ftag] if self.ftag is not None else first_value(self.finish.inputs())) stype = get_instantiated_type(self.source.returntype()) ftype = get_instantiated_type(ftoolin.intype) if self.scatter: if not stype.is_array(): raise Exception( f"Scatter was required for '{self.source} → '{self.finish.id()}.{self.ftag}' but " f"the input type was {type(stype).__name__} and not an array" ) stype = stype.subtype() # Scatters are handled automatically by the StepTagInput Array unwrapping # Merges are handled automatically by the `start_is_scattered` Array wrap self.compatible_types = ftype.can_receive_from(stype, False) if not self.compatible_types: if ftype.is_array() and ftype.subtype().can_receive_from(stype): self.compatible_types = True if not self.compatible_types: s = str(self.source) f = full_dot(self.finish, self.ftag) message = (f"Mismatch of types when joining '{s}' to '{f}': " f"{stype.id()} -/→ {ftoolin.intype.id()}") if stype.is_array() and ftype.can_receive_from(stype.subtype()): message += " (did you forget to SCATTER?)" Logger.critical(message)
def find_connection_inner( self, source_dt: ParseableType, desired_dt: ParseableType) -> Optional[List[JanisTransformation]]: source = get_instantiated_type(source_dt) desired = get_instantiated_type(desired_dt) if desired.can_receive_from(source): return [] queue: List[JanisTransformation] = [] parent_mapping: Dict[str, JanisTransformation] = {} desired_dt_name = desired.name() queue.extend(self._edges.get(source.name(), [])) parent_mapping[source.name()] = None while len(queue) > 0: edge = queue.pop(0) end_name = edge.type2.name() if end_name in parent_mapping: # we've already got a parent (hence we've already seen it), so let's skip it continue parent_mapping[end_name] = edge if end_name == desired_dt_name: return JanisTransformationGraph.trace(parent_mapping, end_name) queue.extend(self._edges.get(end_name, [])) return None
def __init__( self, tag: str, output_type: ParseableType, selector: Optional[Union[Selector, str]] = None, presents_as: str = None, secondaries_present_as: Dict[str, str] = None, doc: Optional[Union[str, OutputDocumentation]] = None, glob: Optional[Union[Selector, str]] = None, _skip_output_quality_check=False, ): """ A ToolOutput instructs the the engine how to collect an output and how it may be referenced in a workflow. :param tag: The identifier of a output, must be unique in the inputs and outputs. :param output_type: The type of output that is being collected. :param selector: How to collect this output, can accept any :class:`janis.Selector`. :param glob: (DEPRECATED) An alias for `selector` :param doc: Documentation on what the output is, used to generate docs. :param _skip_output_quality_check: DO NOT USE THIS PARAMETER, it's a scapegoat for parsing CWL ExpressionTools when an cwl.output.json is generated """ if not Validators.validate_identifier(tag): raise Exception( f"The identifier '{tag}' was invalid because {Validators.reason_for_failure(tag)}" ) self.tag = tag self.output_type: ParseableType = get_instantiated_type(output_type) self._skip_output_quality_check = _skip_output_quality_check if selector is None and glob is not None: selector = glob elif selector is not None and glob is not None: raise TypeError( f"ToolInput({tag}) received inputs for both selector and glob. Please only use glob" ) if (not _skip_output_quality_check and selector is None and not (isinstance(self.output_type, Stdout) or isinstance(self.output_type, Stderr))): raise Exception( "ToolOutput expects a 'selector=' param when the output type is not Stdout / Stderr" ) self.selector = selector self.presents_as = presents_as self.secondaries_present_as = secondaries_present_as self.doc = (doc if isinstance(doc, OutputDocumentation) else OutputDocumentation(doc=doc)) if isinstance(selector, Operator) and self.presents_as: raise Exception( f"Error when constructing output '{self.id()}', Janis does not support 'presents_as' AND " "operators within a ToolOutput selector. Please raise an issue if you think this is in error." ) if self.secondaries_present_as: if not self.output_type.secondary_files(): raise Exception( f"The ToolOutput '{self.id()}' requested a rewrite of secondary file extension through " f"'secondaries_present_as', but the type {self.output_type.id()} not have any secondary files." ) secs = set(self.output_type.secondary_files()) to_remap = set(self.secondaries_present_as.keys()) invalid = to_remap - secs if len(invalid) > 0: raise Exception( f"Error when constructing output '{self.id()}', the secondaries_present_as contained secondary " f"files ({', '.join(invalid)}) that were not found in the output " f"type '{self.output_type.id()}' ({', '.join(secs)})")
def __init__(self, tag, outtype, doc: OutputDocumentation = None): self.tag = tag self.outtype = get_instantiated_type(outtype) self.doc: Optional[OutputDocumentation] = doc
def step( self, identifier: str, tool: Tool, scatter: Union[str, ScatterDescription] = None, ignore_missing=False, ): """ Method to do :param identifier: :param tool: :param scatter: Indicate whether a scatter should occur, on what, and how :type scatter: Union[str, ScatterDescription] :param ignore_missing: Don't throw an error if required params are missing from this function :return: """ self.verify_identifier(identifier, tool.id()) if scatter is not None and isinstance(scatter, str): scatter = ScatterDescription( [scatter] if isinstance(scatter, str) else scatter ) # verify scatter if scatter: ins = set(tool.inputs_map().keys()) fields = set(scatter.fields) if any(f not in ins for f in fields): # if there is a field not in the input map, we have a problem extra_keys = ", ".join(f"'{f}'" for f in (fields - ins)) raise Exception( f"Couldn't scatter the field(s) for step '{identifier}' " f"(tool: '{tool}') {extra_keys} as they were not found in the input map" ) tool.workflow = self inputs = tool.inputs_map() connections = tool.connections provided_keys = set(connections.keys()) all_keys = set(inputs.keys()) required_keys = set( i for i, v in inputs.items() if not v.input_type.optional and not v.default ) if not provided_keys.issubset(all_keys): unrecparams = ", ".join(provided_keys - all_keys) tags = ", ".join([f"in.{i}" for i in all_keys]) raise Exception( f"Unrecognised parameters {unrecparams} when creating '{identifier}' ({tool.id()}). " f"Expected types: {tags}" ) if not ignore_missing and not required_keys.issubset(provided_keys): missing = ", ".join(f"'{i}'" for i in (required_keys - provided_keys)) raise Exception( f"Missing the parameters {missing} when creating '{identifier}' ({tool.id()})" ) stp = StepNode(self, identifier=identifier, tool=tool, scatter=scatter) added_edges = [] for (k, v) in connections.items(): if is_python_primitive(v): inp_identifier = f"{identifier}_{k}" referencedtype = copy.copy(inputs[k].input_type) parsed_type = get_instantiated_type(v) if parsed_type and not referencedtype.can_receive_from(parsed_type): raise TypeError( f"The type {parsed_type.id()} inferred from the value '{v}' is not " f"compatible with the '{identifier}.{k}' type: {referencedtype.id()}" ) referencedtype.optional = True v = self.input(inp_identifier, referencedtype, default=v) if v is None: inp_identifier = f"{identifier}_{k}" v = self.input(inp_identifier, inputs[k].input_type, default=v) verifiedsource = verify_or_try_get_source(v) if isinstance(verifiedsource, list): for vv in verifiedsource: added_edges.append(stp._add_edge(k, vv)) else: added_edges.append(stp._add_edge(k, verifiedsource)) for e in added_edges: si = e.finish.sources[e.ftag] if e.ftag else first_value(e.finish.sources) self.has_multiple_inputs = self.has_multiple_inputs or si.multiple_inputs self.has_scatter = self.has_scatter or scatter is not None self.has_subworkflow = self.has_subworkflow or isinstance(tool, Workflow) self.nodes[identifier] = stp self.step_nodes[identifier] = stp return stp
def test_parse_primitive_bool(self): t = get_instantiated_type(bool) self.assertIsInstance(t, Boolean)
def test_parse_instantiated_bool(self): t = get_instantiated_type(Boolean()) self.assertIsInstance(t, Boolean)
def test_parse_union_type(self): t = get_instantiated_type(Union[str, int]) self.assertIsInstance(t.subtypes[0], String) self.assertIsInstance(t.subtypes[1], Int) self.assertEqual("Union<String, Integer>", t.id())
def test_parse_optional_file(self): t = get_instantiated_type(Optional[File]) self.assertTrue(t.optional) self.assertIsInstance(t, File)
def __init__( self, tag: str, input_type: ParseableType, position: Optional[int] = None, prefix: Optional[str] = None, separate_value_from_prefix: bool = None, prefix_applies_to_all_elements: bool = None, presents_as: str = None, secondaries_present_as: Dict[str, str] = None, separator: str = None, shell_quote: bool = None, localise_file: bool = None, default: Any = None, doc: Optional[Union[str, InputDocumentation]] = None, ): """ A ``ToolInput`` represents an input to a tool, with parameters that allow it to be bound on the command line. The ToolInput must have either a position or prefix set to be bound onto the command line. :param tag: The identifier of the input (unique to inputs and outputs of a tool) :param input_type: The data type that this input accepts :type input_type: ``janis.ParseableType`` :param position: The position of the input to be applied. (Default = 0, after the base_command). :param prefix: The prefix to be appended before the element. (By default, a space will also be applied, see ``separate_value_from_prefix`` for more information) :param separate_value_from_prefix: (Default: True) Add a space between the prefix and value when ``True``. :param prefix_applies_to_all_elements: Applies the prefix to each element of the array (Array inputs only) :param shell_quote: Stops shell quotes from being applied in all circumstances, useful when joining multiple commands together. :param separator: The separator between each element of an array (defaults to ' ') :param localise_file: Ensures that the file(s) are localised into the execution directory. :param default: The default value to be applied if the input is not defined. :param doc: Documentation string for the ToolInput, this is used to generate the tool documentation and provide hints to the user. """ super().__init__( value=None, prefix=prefix, position=position, separate_value_from_prefix=separate_value_from_prefix, doc=None, shell_quote=shell_quote, ) self.doc: InputDocumentation = (doc if isinstance( doc, DocumentationMeta) else InputDocumentation(doc=doc)) # if default is not None: # input_type.optional = True if not Validators.validate_identifier(tag): raise Exception( f"The identifier '{tag}' was not validated because {Validators.reason_for_failure(tag)}" ) self.tag: str = tag self.input_type: ParseableType = get_instantiated_type(input_type) self.default = default self.prefix_applies_to_all_elements = prefix_applies_to_all_elements self.separator = separator self.localise_file = localise_file self.presents_as = presents_as self.secondaries_present_as = secondaries_present_as if self.secondaries_present_as: if not self.input_type.secondary_files(): raise Exception( f"The ToolOutput '{self.id()}' requested a rewrite of secondary file extension through " f"'secondaries_present_as', but the type {self.input_type.id()} not have any secondary files." ) secs = set(self.input_type.secondary_files()) to_remap = set(self.secondaries_present_as.keys()) invalid = to_remap - secs if len(invalid) > 0: raise Exception( f"Error when constructing output '{self.id()}', the secondaries_present_as contained secondary " f"files ({', '.join(invalid)}) that were not found in the output " f"type '{self.input_type.id()}' ({', '.join(secs)})")
def test_parse_instantiated_float(self): t = get_instantiated_type(Float()) self.assertIsInstance(t, Float)