diff --git a/nf_core/components/info.py b/nf_core/components/info.py index 4cf7dc946..ae611324a 100644 --- a/nf_core/components/info.py +++ b/nf_core/components/info.py @@ -240,6 +240,12 @@ def generate_params_table(self, type) -> Table: table.add_column("Structure", justify="right", style="green") return table + def generate_args_table(self) -> Table: + table = Table(expand=True, show_lines=True, box=box.MINIMAL_HEAVY_HEAD, padding=0) + table.add_column(":hammer_and_wrench: Extra Args") + table.add_column("Description") + return table + def get_channel_structure(self, structure: dict) -> str: "Get the structure of a channel" structure_str = "" @@ -341,6 +347,17 @@ def generate_component_info_help(self): renderables.append(outputs_table) + # args + if self.meta.get("args"): + args_table = self.generate_args_table() + for argname, arginfo in self.meta["args"].items(): + args_table.add_row( + f"[orange1 on black] {argname} [/][dim i]", + Markdown(arginfo["description"] if arginfo["description"] else ""), + ) + + renderables.append(args_table) + # Installation command if self.remote_location and not self.local: cmd_base = f"nf-core {self.component_type}" diff --git a/nf_core/components/nfcore_component.py b/nf_core/components/nfcore_component.py index b04cb71e3..32deef3b2 100644 --- a/nf_core/components/nfcore_component.py +++ b/nf_core/components/nfcore_component.py @@ -279,3 +279,24 @@ def get_outputs_from_main_nf(self): pass log.debug(f"Found {len(outputs)} outputs in {self.main_nf}") self.outputs = outputs + + def get_ext_args_from_main_nf(self) -> List[str]: + """Collect all ext.args from the main.nf file.""" + if self.component_type == "modules": + extargs: set[str] = set() + with open(self.main_nf) as f: + data = f.read() + section_identifier = "script:" + if "script:" not in data: + section_identifier = "exec:" + if "exec:" not in data: + log.debug(f"Could not find 'script' or 'exec' section in {self.main_nf}") + return [] + input_data = data.split(section_identifier)[1].split("stub:")[0] + regex = r"ext.(args[0-9]*) " + matches = re.finditer(regex, input_data) + for m in matches: + extargs.add(m.group(1)) + log.debug(f"Found {len(extargs)} extra args in {self.main_nf}") + return sorted(extargs) + return [] diff --git a/nf_core/module-template/meta.yml b/nf_core/module-template/meta.yml index 1227530ef..e1d9f770d 100644 --- a/nf_core/module-template/meta.yml +++ b/nf_core/module-template/meta.yml @@ -22,6 +22,13 @@ tools: licence: {{ tool_licence }} identifier: {{ tool_identifier }} +{% if not_empty_template -%} +## TODO nf-core: Add a description of all of the ext.args used in the pipeline +{% endif -%} +extra_args: + args: + description: "{{ component }}" + {% if not_empty_template -%} ## TODO nf-core: Add a description of all of the variables used as input {% endif -%} diff --git a/nf_core/modules/lint/meta_yml.py b/nf_core/modules/lint/meta_yml.py index 977ea819a..5e061b8b2 100644 --- a/nf_core/modules/lint/meta_yml.py +++ b/nf_core/modules/lint/meta_yml.py @@ -50,7 +50,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m return raise LintExceptionError("Module does not have a `meta.yml` file") # Check if we have a patch file, get original file in that case - meta_yaml = read_meta_yml(module_lint_object, module) + meta_yaml_content = read_meta_yml(module_lint_object, module) if module.is_patched and module_lint_object.modules_repo.repo_path is not None: lines = ComponentsDiffer.try_apply_patch( module.component_type, @@ -62,8 +62,8 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m ).get("meta.yml") if lines is not None: yaml = ruamel.yaml.YAML() - meta_yaml = yaml.load("".join(lines)) - if meta_yaml is None: + meta_yaml_content = yaml.load("".join(lines)) + if meta_yaml_content is None: module.failed.append(("meta_yml_exists", "Module `meta.yml` does not exist", module.meta_yml)) return else: @@ -74,7 +74,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m try: with open(Path(module_lint_object.modules_repo.local_repo_dir, "modules/meta-schema.json")) as fh: schema = json.load(fh) - validators.validate(instance=meta_yaml, schema=schema) + validators.validate(instance=meta_yaml_content, schema=schema) module.passed.append(("meta_yml_valid", "Module `meta.yml` is valid", module.meta_yml)) valid_meta_yml = True except exceptions.ValidationError as e: @@ -85,7 +85,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m hint = f"\nCheck that the child entries of {str(e.path[0]) + '.' + str(e.path[2])} are indented correctly." if e.schema and isinstance(e.schema, dict) and "message" in e.schema: e.message = e.schema["message"] - incorrect_value = meta_yaml + incorrect_value = meta_yaml_content for key in e.path: incorrect_value = incorrect_value[key] @@ -101,7 +101,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m # Confirm that all input and output channels are correctly specified if valid_meta_yml: # confirm that the name matches the process name in main.nf - if meta_yaml["name"].upper() == module.process_name: + if meta_yaml_content["name"].upper() == module.process_name: module.passed.append( ( "meta_name", @@ -113,12 +113,12 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m module.failed.append( ( "meta_name", - f"Conflicting `process` name between meta.yml (`{meta_yaml['name']}`) and main.nf (`{module.process_name}`)", + f"Conflicting `process` name between meta.yml (`{meta_yaml_content['name']}`) and main.nf (`{module.process_name}`)", module.meta_yml, ) ) # Check that inputs are specified in meta.yml - if len(module.inputs) > 0 and "input" not in meta_yaml: + if len(module.inputs) > 0 and "input" not in meta_yaml_content: module.failed.append( ( "meta_input", @@ -137,8 +137,10 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m else: log.debug(f"No inputs specified in module `main.nf`: {module.component_name}") # Check that all inputs are correctly specified - if "input" in meta_yaml: - correct_inputs, meta_inputs = obtain_correct_and_specified_inputs(module_lint_object, module, meta_yaml) + if "input" in meta_yaml_content: + correct_inputs, meta_inputs = obtain_correct_and_specified_inputs( + module_lint_object, module, meta_yaml_content + ) if correct_inputs == meta_inputs: module.passed.append( @@ -158,7 +160,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m ) # Check that outputs are specified in meta.yml - if len(module.outputs) > 0 and "output" not in meta_yaml: + if len(module.outputs) > 0 and "output" not in meta_yaml_content: module.failed.append( ( "meta_output", @@ -175,8 +177,10 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m ) ) # Check that all outputs are correctly specified - if "output" in meta_yaml: - correct_outputs, meta_outputs = obtain_correct_and_specified_outputs(module_lint_object, module, meta_yaml) + if "output" in meta_yaml_content: + correct_outputs, meta_outputs = obtain_correct_and_specified_outputs( + module_lint_object, module, meta_yaml_content + ) if correct_outputs == meta_outputs: module.passed.append( @@ -195,6 +199,26 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m ) ) + # Check that ext.args are listed in the `extra_args` section + expected_extra_args = module.get_ext_args_from_main_nf() + extra_args_yml = sorted(meta_yaml_content.get("extra_args", {}).keys()) + if expected_extra_args == extra_args_yml: + module.passed.append( + ( + "correct_extra_args", + "Correct extra args specified in `meta.yml`", + module.meta_yml, + ) + ) + else: + module.warned.append( + ( + "correct_extra_args", + f"Module `meta.yml` does not match `main.nf`. Extra args should contain: {extra_args_yml}\n", + module.meta_yml, + ) + ) + def read_meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent) -> Union[dict, None]: """