Source code for nitpick.plugins.yaml

"""YAML files."""

from __future__ import annotations

from itertools import chain
from typing import TYPE_CHECKING, ClassVar, Iterator, cast

from nitpick.blender import Comparison, YamlDoc, traverse_yaml_tree
from nitpick.config import SpecialConfig
from nitpick.constants import PRE_COMMIT_CONFIG_YAML
from nitpick.exceptions import Deprecation
from nitpick.plugins import hookimpl
from nitpick.plugins.base import NitpickPlugin
from nitpick.plugins.text import KEY_CONTAINS
from nitpick.violations import Fuss, SharedViolations, ViolationEnum

if TYPE_CHECKING:
    from nitpick.plugins.info import FileInfo
    from nitpick.typedefs import JsonDict, YamlObject

# keep-sorted start
KEY_HOOKS = "hooks"
KEY_ID = "id"
KEY_REPO = "repo"
KEY_REPOS = "repos"
KEY_YAML = "yaml"
# keep-sorted end


[docs]class YamlPlugin(NitpickPlugin): """Enforce configurations and autofix YAML files. - Example: `.pre-commit-config.yaml <https://pre-commit.com/#pre-commit-configyaml---top-level>`_. - Style example: :gitref:`the default pre-commit hooks <src/nitpick/resources/any/pre-commit-hooks.toml>`. .. warning:: The plugin tries to preserve comments in the YAML file by using the ``ruamel.yaml`` package. It works for most cases. If your comment was removed, place them in a different place of the fil and try again. If it still doesn't work, please :issue:`report a bug <new/choose>`. Known issue: lists like ``args`` and ``additional_dependencies`` might be joined in a single line, and comments between items will be removed. Move your comments outside these lists, and they should be preserved. .. note:: No validation of ``.pre-commit-config.yaml`` will be done anymore in this generic YAML plugin. Nitpick will not validate hooks and missing keys as it did before; it's not the purpose of this package. """ identify_tags: ClassVar = {"yaml"} violation_base_code = 360 fixable = True
[docs] def predefined_special_config(self) -> SpecialConfig: """Predefined special config, with list keys for .pre-commit-config.yaml and GitHub Workflow files.""" spc = SpecialConfig() # pylint: disable=assigning-non-slot if self.filename == PRE_COMMIT_CONFIG_YAML: spc.list_keys.from_plugin = {"repos": "hooks.id"} elif self.filename.startswith(".github/workflows"): spc.list_keys.from_plugin = {"jobs.*.steps": "name"} return spc
[docs] def enforce_rules(self) -> Iterator[Fuss]: """Enforce rules for missing data in the YAML file.""" if KEY_CONTAINS in self.expected_config: # If the expected configuration has this key, it means that this config is being handled by TextPlugin. # TODO: fix: allow a YAML file with a "contains" key on its root (how?) return yaml_doc = YamlDoc(path=self.file_path) comparison = Comparison(yaml_doc, self._remove_yaml_subkey(self.expected_config), self.special_config)() if not comparison.has_changes: return yield from chain( self.report(SharedViolations.DIFFERENT_VALUES, yaml_doc.as_object, cast(YamlDoc, comparison.diff)), self.report( SharedViolations.MISSING_VALUES, yaml_doc.as_object, cast(YamlDoc, comparison.missing), cast(YamlDoc, comparison.replace), ), ) if self.autofix and self.dirty: yaml_doc.updater.dump(yaml_doc.as_object, self.file_path)
@staticmethod def _remove_yaml_subkey(old_config: JsonDict) -> JsonDict: """Remove the obsolete "yaml" key that was used in the deprecated ``PreCommitPlugin``.""" if KEY_REPOS not in old_config: return old_config new_config = old_config.copy() new_config[KEY_REPOS] = [] repos_with_yaml_key = False for repo in old_config[KEY_REPOS]: # type: JsonDict new_repo = repo.copy() if KEY_YAML in new_repo: new_repo.pop(KEY_YAML, None) repos_with_yaml_key = True if bool(new_repo): new_config[KEY_REPOS].append(new_repo) if repos_with_yaml_key: Deprecation.pre_commit_repos_with_yaml_key() return new_config
[docs] def report( self, violation: ViolationEnum, yaml_object: YamlObject, change: YamlDoc | None, replacement: YamlDoc | None = None, ): """Report a violation while optionally modifying the YAML document.""" if not (change or replacement): return if self.autofix: real_change = cast(YamlDoc, replacement or change) traverse_yaml_tree(yaml_object, real_change.as_object) self.dirty = True to_display = cast(YamlDoc, change or replacement) yield self.reporter.make_fuss(violation, to_display.reformatted.strip(), prefix="", fixed=self.autofix)
@property def initial_contents(self) -> str: """Suggest the initial content for this missing file.""" return self.write_initial_contents(YamlDoc, self._remove_yaml_subkey(self.expected_config))
[docs]@hookimpl def plugin_class() -> type[NitpickPlugin]: """Handle YAML files.""" return YamlPlugin
[docs]@hookimpl def can_handle(info: FileInfo) -> type[NitpickPlugin] | None: """Handle YAML files.""" if YamlPlugin.identify_tags & info.tags: return YamlPlugin return None