diff --git a/precommend/.pre-commit-config.yaml b/precommend/.pre-commit-config.yaml index 52d9227..395bb38 100644 --- a/precommend/.pre-commit-config.yaml +++ b/precommend/.pre-commit-config.yaml @@ -1,92 +1,75 @@ +# This file contains all the hooks that precommend would ever recommend to users. +# They are kept in this file in order to allow pre-commit CI to update them on a +# regular basis. If you want to add a new hook, add it here and then add a corresponding +# inclusion rule in rules.py. Note that YAML parsing with comment preservation is +# a fickle issue: Only end-of-line comments are safe to use with our toolchain. + repos: - repo: https://github.com/psf/black rev: 23.9.1 hooks: - # Run Black - the uncompromising Python code formatter - - id: black - # Run Black - the uncompromising Python code formatter (Jupyter version) - - id: black-jupyter + - id: black # Run Black - the uncompromising Python code formatter + - id: black-jupyter # Run Black - the uncompromising Python code formatter (Jupyter version) - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - # Ensure existence of newline characters at file ends - - id: end-of-file-fixer - # Make sure that contained YAML files are well-formed - - id: check-yaml - # Trim trailing whitespace of all sorts - - id: trailing-whitespace - # Apply a file size limit of 500kB - - id: check-added-large-files - # Simple parser validation of e.g. pyproject.toml - - id: check-toml - # Unify file endings - - id: end-of-file-fixer - # Sort lines in requirements files - - id: requirements-txt-fixer - # Check validity of JSON files - - id: check-json - # Format JSON files consistently - - id: pretty-format-json + - id: end-of-file-fixer # Ensure existence of newline characters at file ends + - id: check-yaml # Make sure that contained YAML files are well-formed + - id: trailing-whitespace # Trim trailing whitespace of all sorts + - id: check-added-large-files # Apply a file size limit of 500kB + - id: check-toml # Simple parser validation of e.g. pyproject.toml + - id: requirements-txt-fixer # Sort lines in requirements files + - id: check-json # Check validity of JSON files + - id: pretty-format-json # Format JSON files consistently exclude_types: - jupyter args: - --autofix - # Ensure consistent line endings - - id: mixed-line-ending + - id: mixed-line-ending # Ensure consistent line endings - repo: https://github.com/rhysd/actionlint rev: v1.6.25 hooks: - # GitHub Actions Workflow linter - - id: actionlint + - id: actionlint # GitHub Actions Workflow linter - repo: https://github.com/kynan/nbstripout rev: 0.6.1 hooks: - # Make sure that Jupyter notebooks under version control - # have their outputs stripped before committing - - id: nbstripout + - id: nbstripout # Make sure that Jupyter notebooks under version control have their outputs stripped before committing files: ".ipynb" - repo: https://github.com/cheshirekow/cmake-format-precommit rev: v0.6.13 hooks: - # Apply formatting to CMake files - - id: cmake-format + - id: cmake-format # Apply formatting to CMake files additional_dependencies: - - pyyaml - # Apply linting to CMake files - - id: cmake-lint + - pyyaml + - id: cmake-lint # Apply linting to CMake files - repo: https://github.com/pre-commit/mirrors-clang-format rev: v16.0.6 hooks: - # Format C++ code with Clang-Format - automatically applying the changes - - id: clang-format + - id: clang-format # Format C++ code with Clang-Format - automatically applying the changes args: - --style=Mozilla - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.5.0 hooks: - # Automatically format/sanitize setup.cfg - - id: setup-cfg-fmt + - id: setup-cfg-fmt # Automatically format/sanitize setup.cfg - repo: https://github.com/citation-file-format/cffconvert rev: main hooks: - # Validate CFF format - - id: validate-cff + - id: validate-cff # Validate CFF format - repo: https://github.com/lovesegfault/beautysh rev: v6.2.1 hooks: - # Beautify Bash scripts - - id: beautysh + - id: beautysh # Beautify Bash scripts - repo: https://github.com/abravalheri/validate-pyproject rev: v0.15 hooks: - # Validate the contents of pyproject.toml - - id: validate-pyproject + - id: validate-pyproject # Validate the contents of pyproject.toml diff --git a/precommend/__main__.py b/precommend/__main__.py index 0a10193..ff1d178 100644 --- a/precommend/__main__.py +++ b/precommend/__main__.py @@ -5,6 +5,7 @@ ) import os +import ruamel.yaml def main(): @@ -14,8 +15,9 @@ def main(): if os.path.exists(path): raise IOError("Pre-commit config already present, no upgrade support yet.") + yaml = ruamel.yaml.YAML() with open(path, "w") as f: - f.write(config.as_yaml()) + yaml.dump(config, f) if __name__ == "__main__": diff --git a/precommend/core.py b/precommend/core.py index e0f0727..951e48e 100644 --- a/precommend/core.py +++ b/precommend/core.py @@ -1,7 +1,7 @@ import copy import functools import os -import strictyaml +import ruamel.yaml from pre_commit.git import get_all_files from identify.identify import tags_from_path @@ -40,38 +40,30 @@ def collect_hooks(ctx): def generate_config(hooks): + # Load all available hooks + yaml = ruamel.yaml.YAML() with open( os.path.join(os.path.dirname(__file__), ".pre-commit-config.yaml"), "r" ) as f: - data = strictyaml.load(f.read()) + data = yaml.load(f) - # NB: The deepcopy's here might seem unnecessary, but they are required - # because modification of strictyaml objects is a difficult process. - output = copy.deepcopy(data) + # Ruamel types to use in constructing the output + CS = ruamel.yaml.comments.CommentedSeq + CM = ruamel.yaml.comments.CommentedMap - def _remove_hook(hook_id): - for i, orepo in enumerate(output["repos"]): - for j, ohook in enumerate(orepo["hooks"]): - if ohook.value["id"] == hook_id: - del output["repos"][i]["hooks"][j] - return - - # Iterate the original data and remove hooks + # The output data structure + final = CM(repos=CS()) for repo in data["repos"]: - for hook in repo["hooks"]: - if hook.value["id"] not in hooks: - _remove_hook(hook.value["id"]) - - output2 = copy.deepcopy(output) - - def _remove_repo(repo_url): - for i, repo in enumerate(output2["repos"]): - if repo.value["repo"] == repo_url: - del output2["repos"][i] - return - - for repo in output["repos"]: - if len(repo["hooks"].value) == 0: - _remove_repo(repo.value["repo"]) - - return output2 + # If no hook of this repo matches, it is never added to the output + if set(h["id"] for h in repo["hooks"]).intersection(hooks): + repo_hooks = repo.pop("hooks") + hook_list = CS() + for hook in repo_hooks: + if hook["id"] in hooks: + hook_list.append(hook) + + repo_map = CM(**repo) + repo_map["hooks"] = hook_list + final["repos"].append(repo_map) + + return final diff --git a/pyproject.toml b/pyproject.toml index 65d88ff..1ccc181 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "click", "identify", "pre-commit", - "strictyaml", + "ruamel.yaml", ] [project.optional-dependencies] diff --git a/tests/test_core.py b/tests/test_core.py index 347dff2..886340e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,7 +4,6 @@ def test_collect_hooks_python(monkeypatch, python_data): monkeypatch.chdir(python_data) ctx = GenerationContext() - print(ctx._files) hooks = collect_hooks(ctx) assert "black" in hooks @@ -26,10 +25,10 @@ def test_generate_config_python(monkeypatch, tmp_path, python_data): hooks = collect_hooks(ctx) monkeypatch.chdir(str(tmp_path)) - output = generate_config(hooks).as_yaml() + output = generate_config(hooks) - assert "black" in output - assert "validate-pyproject" in output + assert "black" in str(output) + assert "validate-pyproject" in str(output) def test_generate_config_python(monkeypatch, tmp_path, cpp_data): @@ -38,7 +37,7 @@ def test_generate_config_python(monkeypatch, tmp_path, cpp_data): hooks = collect_hooks(ctx) monkeypatch.chdir(str(tmp_path)) - output = generate_config(hooks).as_yaml() + output = generate_config(hooks) - assert "clang-format" in output - assert "cmake-format" in output + assert "clang-format" in str(output) + assert "cmake-format" in str(output)