diff --git a/nf_core/launch.py b/nf_core/launch.py index f3bf2cf2df..f527b75a0d 100644 --- a/nf_core/launch.py +++ b/nf_core/launch.py @@ -16,7 +16,7 @@ def launch_pipeline(workflow, params_local_uri, direct): params_list = [] try: if params_local_uri: - with open(params_local_uri, 'r') as fp: params_json_str = fp.read() + with open(params_local_uri, 'r') as fp: params_json_str = fp.read() else: params_json_str = nf_core.utils.fetch_parameter_settings_from_github(workflow) params_list = pms.Parameters.create_from_json(params_json_str) @@ -43,10 +43,10 @@ def __init__(self, workflow): self.workflow = workflow self.nxf_flag_defaults = { - '-name': False, - '-r': False, + '-name': None, + '-r': None, '-profile': 'standard', - '-w': os.getenv('NXF_WORK') if os.getenv('NXF_WORK') else 'work', + '-w': os.getenv('NXF_WORK') if os.getenv('NXF_WORK') else './work', '-resume': False } self.nxf_flag_help = { @@ -72,9 +72,29 @@ def collect_defaults(self): def prompt_vars(self, params = None, direct = False): """ Ask the user if they want to override any default values """ # Main nextflow flags - logging.info("Main nextflow options:\n") + click.secho("Main nextflow options", bold=True, underline=True) for flag, f_default in self.nxf_flag_defaults.items(): - f_user = click.prompt(self.nxf_flag_help[flag], default=f_default) + + # Click prompts don't like None, so we have to use an empty string instead + f_default_print = f_default + if f_default is None: + f_default = '' + f_default_print = 'None' + + # Overwrite the default prompt for boolean + if isinstance(f_default, bool): + f_default_print = 'Y/n' if f_default else 'y/N' + + # Prompt for a response + f_user = click.prompt( + "\n{}\n {} {}".format( + self.nxf_flag_help[flag], + click.style(flag, fg='blue'), + click.style('[{}]'.format(str(f_default_print)), fg='green') + ), + default = f_default, + show_default = False + ) # Only save if we've changed the default if f_user != f_default: # Convert string bools to real bools @@ -85,14 +105,13 @@ def prompt_vars(self, params = None, direct = False): except AttributeError: pass self.nxf_flags[flag] = f_user - + # Uses the parameter values from the JSON file # and does not ask the user to set them explicitly if direct: return # Pipeline params - logging.info("Pipeline specific parameters:\n") if params: Launch.__prompt_defaults_from_param_objects( Launch.__group_parameters(params) @@ -143,39 +162,69 @@ def __prompt_defaults_from_param_objects(cls, params_grouped): } """ for group_label, params in params_grouped.items(): - print("{prespace}Parameter group: \'{label}\'{postspace}".format( - prespace='-'*5, - label=group_label, - postspace='-'*10) - ) - use_defaults = click.confirm("Do you want to use the group's defaults?", - default=True) - if use_defaults: + click.echo("\n\n{}{}".format( + click.style('Parameter group: ', bold=True, underline=True), + click.style(group_label, bold=True, underline=True, fg='red'), + )) + use_defaults = click.confirm( + "Do you want to change the group's defaults? "+click.style('[y/N]', fg='green'), + default=False, show_default=False) + if not use_defaults: continue for parameter in params: value_is_valid = False - while(not value_is_valid): - desired_param_value = click.prompt("'--{name}'\n" - "\tUsage: {usage}\n" - "\tRange/Choices: {choices}.\n" - .format(name=parameter.name, - usage=parameter.usage, - choices=parameter.choices), - default=parameter.default_value) + first_attempt = True + while not value_is_valid: + # Start building the string to show to the user - label and usage + plines = ['', + click.style(parameter.label, bold=True), + click.style(parameter.usage, dim=True) + ] + # Add the choices / range if applicable + if parameter.choices: + rc = 'Choices' if parameter.type == 'string' else 'Range' + plines.append('{}: {}'.format(rc, str(parameter.choices))) + + # Reset the choice display if boolean + if parameter.type == "boolean": + pdef_val = 'Y/n' if parameter.default_value else 'y/N' + else: + pdef_val = parameter.default_value + + # Final line to print - command and default + flag_prompt = click.style(' --{} '.format(parameter.name), fg='blue') + \ + click.style('[{}]'.format(pdef_val), fg='green') + # Only show this final prompt if we're trying again + if first_attempt: + plines.append(flag_prompt) + else: + plines = [flag_prompt] + first_attempt = False + + # Use click.confirm if a boolean for default input handling + if parameter.type == "boolean": + parameter.value = click.confirm("\n".join(plines), + default=parameter.default_value, show_default=False) + # Use click.prompt if anything else + else: + parameter.value = click.prompt("\n".join(plines), + default=parameter.default_value, show_default=False) + + # Set input parameter types if parameter.type == "integer": - desired_param_value = int(desired_param_value) - elif parameter.type == "boolean": - desired_param_value = (str(desired_param_value).lower() in ['yes', 'y', 'true', 't', '1']) + parameter.value = int(parameter.value) elif parameter.type == "decimal": - desired_param_value = float(desired_param_value) - parameter.value = desired_param_value + parameter.value = float(parameter.value) + + # Validate the input try: parameter.validate() except Exception as e: - print(e) + click.secho("\nERROR: {}".format(e), fg='red') + click.secho("Please try again:") continue else: - value_is_valid = True + value_is_valid = True def build_command(self, params = None): """ Build the nextflow run command based on what we know """ @@ -189,13 +238,13 @@ def build_command(self, params = None): # String values else: self.nextflow_cmd = '{} {} "{}"'.format(self.nextflow_cmd, flag, val.replace('"', '\\"')) - + if params: # When a parameter specification file was used, we can run Nextflow with it path = Launch.__create_nfx_params_file(params) self.nextflow_cmd = '{} {} "{}"'.format(self.nextflow_cmd, "--params-file", path) Launch.__write_params_as_full_json(params) return - + for param, val in self.params.items(): # Boolean flags like --saveTrimmed if isinstance(val, bool): @@ -215,7 +264,7 @@ def __create_nfx_params_file(cls, params): with open(output_file, "w") as fp: fp.write(json_string) return output_file - + @classmethod def __write_params_as_full_json(cls, params, outdir = os.getcwd()): output_file = os.path.join(outdir, "full-params.json") @@ -226,7 +275,13 @@ def __write_params_as_full_json(cls, params, outdir = os.getcwd()): def launch_workflow(self): """ Launch nextflow if required """ - logging.info("Nextflow command:\n {}\n\n".format(self.nextflow_cmd)) - if click.confirm('Do you want to run this command now?'): - logging.info("Launching!") + click.secho("\n\nNextflow command:", bold=True, underline=True) + click.secho(" {}\n\n".format(self.nextflow_cmd), fg='magenta') + + if click.confirm( + 'Do you want to run this command now? '+click.style('[y/N]', fg='green'), + default=False, + show_default=False + ): + logging.info("Launching workflow!") subprocess.call(self.nextflow_cmd, shell=True) diff --git a/nf_core/workflow/parameters.py b/nf_core/workflow/parameters.py index b4a3adc161..6149237a1e 100644 --- a/nf_core/workflow/parameters.py +++ b/nf_core/workflow/parameters.py @@ -34,17 +34,17 @@ def create_from_json(parameters_json, schema = ""): properties = json.loads(parameters_json) parameters = [] for param in properties.get("parameters"): - parameter = Parameter.builder().name(param.get("name")) \ - .label(param.get("label")) \ - .usage(param.get("usage")) \ - .param_type(param.get("type")) \ - .choices(param.get("choices")) \ - .default(param.get("default_value")) \ - .pattern(param.get("pattern")) \ - .render(param.get("render")) \ - .arity(param.get("arity")) \ - .group(param.get("group")) \ - .build() + parameter = (Parameter.builder().name(param.get("name")) + .label(param.get("label")) + .usage(param.get("usage")) + .param_type(param.get("type")) + .choices(param.get("choices")) + .default(param.get("default_value")) + .pattern(param.get("pattern")) + .render(param.get("render")) + .arity(param.get("arity")) + .group(param.get("group")) + .build()) parameters.append(parameter) return parameters @@ -63,10 +63,10 @@ def in_nextflow_json(parameters, indent=0): for p in parameters: params[p.name] = p.value if p.value else p.default_value return json.dumps(params, indent=indent) - + @staticmethod def in_full_json(parameters, indent=0): - """Converts a list of Parameter objects into JSON. All attributes + """Converts a list of Parameter objects into JSON. All attributes are written. Args: @@ -79,7 +79,7 @@ def in_full_json(parameters, indent=0): params_dict = {} params_dict["parameters"] = [p.as_dict() for p in parameters] return json.dumps(params_dict, indent=indent) - + @classmethod def __download_schema_from_nf_core(cls, url): with requests_cache.disabled(): @@ -95,7 +95,7 @@ class Parameter(object): """ def __init__(self, param_builder): # Make some checks - + # Put content self.name = param_builder.p_name self.label = param_builder.p_label @@ -109,7 +109,7 @@ def __init__(self, param_builder): self.required = param_builder.p_required self.render = param_builder.p_render self.group = param_builder.p_group - + @staticmethod def builder(): return ParameterBuilder() @@ -121,13 +121,13 @@ def as_dict(self): dict: Parameter object as key value pairs. """ params_dict = {} - for attribute in ['name', 'label', 'usage', 'required', + for attribute in ['name', 'label', 'usage', 'required', 'type', 'value', 'choices', 'default_value', 'pattern', 'arity', 'render', 'group']: if getattr(self, attribute): params_dict[attribute] = getattr(self, attribute) params_dict['required'] = getattr(self, 'required') return params_dict - + def validate(self): """Validates the parameter's value. If the value is within the parameter requirements, no exception is thrown. @@ -135,7 +135,7 @@ def validate(self): Raises: LookupError: Raised when no matching validator can be determined. AttributeError: Raised with description, if a parameter value violates - the parameter constrains. + the parameter constrains. """ validator = vld.Validators.get_validator_for_param(self) validator.validate() @@ -166,7 +166,7 @@ def group(self, group): """ self.p_group = group return self - + def name(self, name): """Sets the parameter name. @@ -175,7 +175,7 @@ def name(self, name): """ self.p_name = name return self - + def label(self, label): """Sets the parameter label. @@ -184,7 +184,7 @@ def label(self, label): """ self.p_label = label return self - + def usage(self, usage): """Sets the parameter usage. @@ -193,7 +193,7 @@ def usage(self, usage): """ self.p_usage = usage return self - + def value(self, value): """Sets the parameter value. @@ -202,7 +202,7 @@ def value(self, value): """ self.p_value = value return self - + def choices(self, choices): """Sets the parameter value choices. @@ -211,7 +211,7 @@ def choices(self, choices): """ self.p_choices = choices return self - + def param_type(self, param_type): """Sets the parameter type. @@ -220,7 +220,7 @@ def param_type(self, param_type): """ self.p_type = param_type return self - + def default(self, default): """Sets the parameter default value. @@ -229,7 +229,7 @@ def default(self, default): """ self.p_default_value = default return self - + def pattern(self, pattern): """Sets the parameter regex pattern. @@ -238,7 +238,7 @@ def pattern(self, pattern): """ self.p_pattern = pattern return self - + def arity(self, arity): """Sets the parameter regex pattern. @@ -256,12 +256,12 @@ def render(self, render): """ self.p_render = render return self - + def required(self, required): """Sets the required parameter flag.""" self.p_required = required return self - + def build(self): """Builds parameter object. @@ -269,4 +269,3 @@ def build(self): Parameter: Fresh from the factory. """ return Parameter(self) - diff --git a/nf_core/workflow/validation.py b/nf_core/workflow/validation.py index 28efa7b599..5954c690bf 100644 --- a/nf_core/workflow/validation.py +++ b/nf_core/workflow/validation.py @@ -9,20 +9,20 @@ ABC = abc.ABCMeta('ABC', (), {}) class Validators(object): - """Gives access to a factory method for objects of instance - :class:`Validator` which returns the correct Validator for a + """Gives access to a factory method for objects of instance + :class:`Validator` which returns the correct Validator for a given parameter type. """ def __init__(self): pass - + @staticmethod def get_validator_for_param(parameter): """Determines matching :class:`Validator` instance for a given parameter. Returns: Validator: Matching validator for a given :class:`Parameter`. - + Raises: LookupError: In case no matching validator for a given parameter type can be determined. @@ -34,11 +34,11 @@ def get_validator_for_param(parameter): elif parameter.type == "boolean": return BooleanValidator(parameter) elif parameter.type == "decimal": - return DecimalValidator(parameter) + return DecimalValidator(parameter) raise LookupError("Cannot find a matching validator for type '{}'." .format(parameter.type)) - + class Validator(ABC): """Abstract base class for different parameter validators. """ @@ -58,10 +58,10 @@ def validate(self): class IntegerValidator(Validator): """Implementation for parameters of type integer. - + Args: parameter (:class:`Parameter`): A Parameter object. - + Raises: AttributeError: In case the argument is not of instance integer. """ @@ -78,7 +78,7 @@ def validate(self): """ value = self._param.value if not isinstance(value, int): - raise AttributeError("The value {} for parameter {} needs to be of type Integer, but was {}" + raise AttributeError("The value {} for parameter {} needs to be an Integer, but was a {}" .format(value, self._param.name, type(value))) choices = sorted([x for x in self._param.choices]) print(choices) @@ -87,16 +87,16 @@ def validate(self): if len(choices) < 2: raise AttributeError("The property 'choices' must have at least two entries.") if not (value >= choices[0] and value <= choices[-1]): - raise AttributeError("The value for parameter '{}' must be within range of [{},{}]" + raise AttributeError("'{}' must be within the range [{},{}]" .format(self._param.name, choices[0], choices[-1])) class StringValidator(Validator): """Implementation for parameters of type string. - + Args: parameter (:class:`Parameter`): A Parameter object. - + Raises: AttributeError: In case the argument is not of instance string. """ @@ -122,24 +122,24 @@ def validate(self): "because the value for 'choices' and 'pattern' were empty.".format(self._param.value)) result = re.match(self._param.pattern, self._param.value) if not result: - raise AttributeError("The value '{}' for parameter '{}'" \ - " did not match the regex pattern '{}' .".format( - self._param.value, self._param.name, self._param.pattern + raise AttributeError("'{}' doesn't match the regex pattern '{}'".format( + self._param.value, self._param.pattern )) else: if value not in choices: - raise AttributeError("The value '{}' for parameter '{}'" \ - " was not part of choices '{}'.".format( - value, self._param.name, choices - )) + raise AttributeError( + "'{}' is not not one of the choices {}".format( + value, str(choices) + ) + ) class BooleanValidator(Validator): """Implementation for parameters of type boolean. - + Args: parameter (:class:`Parameter`): A Parameter object. - + Raises: AttributeError: In case the argument is not of instance boolean. """ @@ -162,10 +162,10 @@ def validate(self): class DecimalValidator(Validator): """Implementation for parameters of type boolean. - + Args: parameter (:class:`Parameter`): A Parameter object. - + Raises: AttributeError: In case the argument is not of instance decimal. """