diff --git a/CHANGELOG.md b/CHANGELOG.md index ad646d22a6..f44da030a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ ### Modules +- sort meta.yml based on keys in schema ([#3958](https://github.com/nf-core/tools/pull/3958)) + ### Subworkflows - Update to new topic version handling ([#3929](https://github.com/nf-core/tools/pull/3929)) diff --git a/nf_core/modules/lint/__init__.py b/nf_core/modules/lint/__init__.py index 71a79d0de5..b05d2623e1 100644 --- a/nf_core/modules/lint/__init__.py +++ b/nf_core/modules/lint/__init__.py @@ -6,10 +6,13 @@ nf-core modules lint """ +import json import logging import os import re +from collections.abc import Mapping from pathlib import Path +from typing import Any import questionary import rich @@ -79,6 +82,7 @@ def __init__( registry=registry, hide_progress=hide_progress, ) + self.meta_schema: Mapping[str, Any] | None = None def lint( self, @@ -287,6 +291,28 @@ def lint_module( self.failed += [LintResult(mod, *m) for m in mod.failed] + def load_meta_schema(self) -> Mapping[str, Any]: + """ + Load the meta.yml JSON schema from the local modules repository cache. + The schema is cached in self.meta_schema to avoid reloading. + + Returns: + dict: The meta.yml JSON schema + + Raises: + LookupError: If the local module cache is not found + """ + # Return cached schema if already loaded + if self.meta_schema is not None: + return self.meta_schema + + if self.modules_repo.local_repo_dir is None: + raise LookupError("Local module cache not found") + + with open(Path(self.modules_repo.local_repo_dir, "modules/meta-schema.json")) as fh: + self.meta_schema = json.load(fh) + return self.meta_schema + def update_meta_yml_file(self, mod): """ Update the meta.yml file with the correct inputs and outputs @@ -324,33 +350,25 @@ def _find_meta_info(meta_yml: dict, element_name: str, is_output=False) -> dict: return {} def _sort_meta_yml(meta_yml: dict) -> dict: - """Ensure topics comes after input/output and before authors""" - # Early return if no topics to reorder - if "topics" not in meta_yml: - return meta_yml - - result = {} - topics_value = meta_yml["topics"] - topics_added = False - - for key, value in meta_yml.items(): - if key == "topics": - continue # Skip topics, we'll add it in the right place - - # Add topics before authors key (if not already added) - if key == "authors" and not topics_added: - result["topics"] = topics_value - topics_added = True - - result[key] = value - - # Add topics after output (preferred) or after input (if no output) - if key == "output": - result["topics"] = topics_value - topics_added = True - elif key == "input" and "output" not in meta_yml: - result["topics"] = topics_value - topics_added = True + """Sort meta.yml keys according to the schema's property order""" + # Get the schema to determine the correct key order + try: + schema = self.load_meta_schema() + schema_keys = list(schema["properties"].keys()) + except (LintExceptionError, KeyError) as e: + raise UserWarning("Failed to load meta schema", e) + + result: dict = {} + + # First, add keys in the order they appear in the schema + for key in schema_keys: + if key in meta_yml: + result[key] = meta_yml[key] + + # Then add any keys that aren't in the schema (to preserve custom keys) + for key in meta_yml.keys(): + if key not in result: + result[key] = meta_yml[key] return result diff --git a/nf_core/modules/lint/meta_yml.py b/nf_core/modules/lint/meta_yml.py index ac65b6677d..a3ce3e7381 100644 --- a/nf_core/modules/lint/meta_yml.py +++ b/nf_core/modules/lint/meta_yml.py @@ -1,6 +1,8 @@ -import json +from __future__ import annotations + import logging from pathlib import Path +from typing import TYPE_CHECKING import ruamel.yaml from jsonschema import exceptions, validators @@ -9,10 +11,13 @@ from nf_core.components.lint import ComponentLint, LintExceptionError from nf_core.components.nfcore_component import NFCoreComponent +if TYPE_CHECKING: + from nf_core.modules.lint import ModuleLint + log = logging.getLogger(__name__) -def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_missing: bool = False) -> None: +def meta_yml(module_lint_object: ModuleLint, module: NFCoreComponent, allow_missing: bool = False) -> None: """ Lint a ``meta.yml`` file @@ -76,8 +81,7 @@ def meta_yml(module_lint_object: ComponentLint, module: NFCoreComponent, allow_m # Confirm that the meta.yml file is valid according to the JSON schema valid_meta_yml = False try: - with open(Path(module_lint_object.modules_repo.local_repo_dir, "modules/meta-schema.json")) as fh: - schema = json.load(fh) + schema = module_lint_object.load_meta_schema() validators.validate(instance=meta_yaml, schema=schema) module.passed.append(("meta_yml", "meta_yml_valid", "Module `meta.yml` is valid", module.meta_yml)) valid_meta_yml = True