diff --git a/.vscode/settings.json b/.vscode/settings.json index cebcfeb4..e099378f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -23,11 +23,11 @@ "[yaml]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "cSpell.enabled": true, "coverage-gutters.coverageFileNames": ["coverage.xml"], "coverage-gutters.coverageReportFileName": "**/htmlcov/index.html", "coverage-gutters.showGutterCoverage": false, "coverage-gutters.showLineCoverage": true, + "cSpell.enabled": true, "editor.formatOnSave": true, "editor.rulers": [88], "files.watcherExclude": { diff --git a/src/repoma/check_dev_files/black.py b/src/repoma/check_dev_files/black.py index b66d0504..fe14ac8c 100644 --- a/src/repoma/check_dev_files/black.py +++ b/src/repoma/check_dev_files/black.py @@ -1,19 +1,19 @@ """Check :file:`pyproject.toml` black config.""" -from collections import OrderedDict from textwrap import dedent from typing import List, Optional -import toml +from ruamel.yaml.comments import CommentedMap from repoma.errors import PrecommitError from repoma.utilities import CONFIG_PATH, natural_sorting from repoma.utilities.executor import Executor from repoma.utilities.precommit import ( - Hook, - PrecommitConfig, - asdict, + find_repo, load_round_trip_precommit_config, + update_precommit_hook, + update_single_hook_precommit_repo, ) +from repoma.utilities.pyproject import load_pyproject from repoma.utilities.setup_cfg import get_supported_python_versions @@ -26,12 +26,14 @@ def main() -> None: executor(_check_activate_preview, config) executor(_check_option_ordering, config) executor(_check_target_versions, config) - executor(_update_nbqa_hook) + executor(_check_pyproject) + executor(_update_precommit_repo) + executor(_update_precommit_nbqa_hook) executor.finalize() def _load_black_config(content: Optional[str] = None) -> dict: - config = _load_pyproject_toml(content) + config = load_pyproject(content) return config.get("tool", {}).get("black", {}) @@ -86,31 +88,31 @@ def _check_target_versions(config: dict) -> None: raise PrecommitError(error_message) -def _update_nbqa_hook() -> None: - repo_url = "https://github.com/nbQA-dev/nbQA" - precommit_config = PrecommitConfig.load() - repo = precommit_config.find_repo(repo_url) - if repo is None: - return +def _update_precommit_repo() -> None: + expected_hook = CommentedMap( + repo="https://github.com/psf/black", + hooks=[CommentedMap(id="black")], + ) + update_single_hook_precommit_repo(expected_hook) + - hook_id = "nbqa-black" - expected = Hook( - hook_id, - additional_dependencies=["black>=22.1.0"], +def _update_precommit_nbqa_hook() -> None: + update_precommit_hook( + repo_url="https://github.com/nbQA-dev/nbQA", + expected_hook=CommentedMap( + id="nbqa-black", + additional_dependencies=["black>=22.1.0"], + ), ) - repo_index = precommit_config.get_repo_index(repo_url) - hook_index = repo.get_hook_index(hook_id) - if hook_index is None: - config, yaml = load_round_trip_precommit_config() - config["repos"][repo_index]["hooks"].append(asdict(expected)) - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Added {hook_id} to pre-commit config") - - if repo.hooks[hook_index] != expected: - config, yaml = load_round_trip_precommit_config() - config["repos"][repo_index]["hooks"][hook_index] = asdict(expected) - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Updated args of {hook_id} pre-commit hook") + + +def _check_pyproject() -> None: + if not CONFIG_PATH.precommit.exists(): + return + config, _ = load_round_trip_precommit_config() + nbqa_repo = find_repo(config, "https://github.com/nbQA-dev/nbQA") + if nbqa_repo is None: + return nbqa_config = _load_nbqa_black_config() if nbqa_config != ["--line-length=85"]: error_message = dedent(""" @@ -128,12 +130,5 @@ def _update_nbqa_hook() -> None: def _load_nbqa_black_config(content: Optional[str] = None) -> List[str]: # cspell:ignore addopts - config = _load_pyproject_toml(content) + config = load_pyproject(content) return config.get("tool", {}).get("nbqa", {}).get("addopts", {}).get("black", {}) - - -def _load_pyproject_toml(content: Optional[str] = None) -> dict: - if content is None: - with open(CONFIG_PATH.pyproject) as stream: - return toml.load(stream, _dict=OrderedDict) - return toml.loads(content, _dict=OrderedDict) diff --git a/src/repoma/check_dev_files/cspell.py b/src/repoma/check_dev_files/cspell.py index 67135dc0..ce9dea23 100644 --- a/src/repoma/check_dev_files/cspell.py +++ b/src/repoma/check_dev_files/cspell.py @@ -9,16 +9,15 @@ from pathlib import Path from typing import Any, Iterable, List, Sequence, Union -import yaml +from ruamel.yaml.comments import CommentedMap from repoma.errors import PrecommitError from repoma.utilities import CONFIG_PATH, REPOMA_DIR, rename_file, vscode from repoma.utilities.executor import Executor from repoma.utilities.precommit import ( PrecommitConfig, - Repo, - fromdict, load_round_trip_precommit_config, + update_single_hook_precommit_repo, ) from repoma.utilities.readme import add_badge, remove_badge @@ -49,7 +48,7 @@ def main() -> None: if repo is None: executor(_remove_configuration) else: - executor(_check_check_hook_options) + executor(_update_precommit_repo) executor(_fix_config_content) executor(_sort_config_entries) executor(add_badge, __BADGE) @@ -98,23 +97,12 @@ def _remove_configuration() -> None: executor.finalize() -def _check_check_hook_options() -> None: - config = PrecommitConfig.load() - existing = config.find_repo(__REPO_URL) - if existing is None: - raise PrecommitError(f"{CONFIG_PATH.precommit} is missing a repo: {__REPO_URL}") - expected_yaml = f""" - - repo: {__REPO_URL} - rev: ... - hooks: - - id: cspell - """ - expected = fromdict(yaml.safe_load(expected_yaml)[0], Repo) - existing.rev = expected.rev - if existing != expected: - raise PrecommitError( - "cSpell pre-commit hook should have the following form:\n" + expected_yaml - ) +def _update_precommit_repo() -> None: + expected_hook = CommentedMap( + repo=__REPO_URL, + hooks=[CommentedMap(id="cspell")], + ) + update_single_hook_precommit_repo(expected_hook) def _fix_config_content() -> None: diff --git a/src/repoma/check_dev_files/editorconfig.py b/src/repoma/check_dev_files/editorconfig.py index 52ba2115..dd57589a 100644 --- a/src/repoma/check_dev_files/editorconfig.py +++ b/src/repoma/check_dev_files/editorconfig.py @@ -5,20 +5,13 @@ `_. """ -from functools import lru_cache from textwrap import dedent -from ruamel.yaml.comments import CommentedMap, CommentedSeq -from ruamel.yaml.scalarstring import DoubleQuotedScalarString, FoldedScalarString +from ruamel.yaml.comments import CommentedMap +from ruamel.yaml.scalarstring import FoldedScalarString -from repoma.errors import PrecommitError from repoma.utilities import CONFIG_PATH -from repoma.utilities.precommit import find_repo, load_round_trip_precommit_config - -__EDITORCONFIG_HOOK_ID = "editorconfig-checker" -__EDITORCONFIG_URL = ( - "https://github.com/editorconfig-checker/editorconfig-checker.python" -) +from repoma.utilities.precommit import update_single_hook_precommit_repo def main() -> None: @@ -27,67 +20,20 @@ def main() -> None: def _update_precommit_config() -> None: - if not CONFIG_PATH.precommit.exists(): - return - expected_hook = __get_expected_hook_definition() - existing_config, yaml = load_round_trip_precommit_config() - repos: CommentedSeq = existing_config.get("repos", []) - idx_and_repo = find_repo(existing_config, __EDITORCONFIG_URL) - if idx_and_repo is None: - idx = __determine_expected_index(existing_config) - repos.insert(idx, expected_hook) - repos.yaml_set_comment_before_after_key( - idx if idx + 1 == len(repos) else idx + 1, - before="\n", - ) - yaml.dump(existing_config, CONFIG_PATH.precommit) - raise PrecommitError(f"Added editorconfig hook to {CONFIG_PATH.precommit}") - idx, existing_hook = idx_and_repo - if not __is_equivalent(existing_hook, expected_hook): - existing_rev = existing_hook.get("rev") - if existing_rev is not None: - expected_hook["rev"] = existing_rev - repos[idx] = expected_hook - repos.yaml_set_comment_before_after_key(idx + 1, before="\n") - yaml.dump(existing_config, CONFIG_PATH.precommit) - raise PrecommitError(f"Updated editorconfig hook in {CONFIG_PATH.precommit}") - - -@lru_cache(maxsize=None) -def __get_expected_hook_definition() -> CommentedMap: - excludes = R""" + excludes = dedent(R""" (?x)^( .*\.py )$ - """ - excludes = dedent(excludes).strip() - hook = { - "id": __EDITORCONFIG_HOOK_ID, - "name": "editorconfig", - "alias": "ec", - "exclude": FoldedScalarString(excludes), - } - dct = { - "repo": __EDITORCONFIG_URL, - "rev": DoubleQuotedScalarString(""), - "hooks": [CommentedMap(hook)], - } - return CommentedMap(dct) - - -def __determine_expected_index(config: CommentedMap) -> int: - repos: CommentedSeq = config["repos"] - for i, repo_def in enumerate(repos): - hook_id: str = repo_def["hooks"][0]["id"] - if __EDITORCONFIG_HOOK_ID.lower() <= hook_id.lower(): - return i - return len(repos) - - -def __is_equivalent(expected: CommentedMap, existing: CommentedMap) -> bool: - def remove_rev(config: CommentedMap) -> dict: - config_copy = dict(config) - config_copy.pop("rev", None) - return config_copy - - return remove_rev(expected) == remove_rev(existing) + """).strip() + expected_hook = CommentedMap( + repo="https://github.com/editorconfig-checker/editorconfig-checker.python", + hooks=[ + CommentedMap( + id="editorconfig-checker", + name="editorconfig", + alias="ec", + exclude=FoldedScalarString(excludes), + ) + ], + ) + update_single_hook_precommit_repo(expected_hook) diff --git a/src/repoma/check_dev_files/nbstripout.py b/src/repoma/check_dev_files/nbstripout.py index a4e5feaf..a05ed8c9 100644 --- a/src/repoma/check_dev_files/nbstripout.py +++ b/src/repoma/check_dev_files/nbstripout.py @@ -1,72 +1,54 @@ """Check the nbstripout hook in the pre-commit config.""" -from typing import Optional, Tuple +from ruamel.yaml.comments import CommentedMap from ruamel.yaml.scalarstring import LiteralScalarString -from repoma.errors import PrecommitError from repoma.utilities import CONFIG_PATH -from repoma.utilities.precommit import PrecommitConfig, Repo -from repoma.utilities.yaml import create_prettier_round_trip_yaml - -# cspell:ignore nbconvert showmarkdowntxt -__REPO_URL = "https://github.com/kynan/nbstripout" -__HOOK_ID = "nbstripout" -__EXTRA_KEYS_ARGUMENT = [ - "cell.attachments", - "cell.metadata.code_folding", - "cell.metadata.id", - "cell.metadata.user_expressions", - "metadata.celltoolbar", - "metadata.colab.name", - "metadata.colab.provenance", - "metadata.interpreter", - "metadata.notify_time", - "metadata.toc", - "metadata.toc-autonumbering", - "metadata.toc-showcode", - "metadata.toc-showmarkdowntxt", - "metadata.toc-showtags", - "metadata.varInspector", - "metadata.vscode", -] +from repoma.utilities.precommit import ( + find_repo, + load_round_trip_precommit_config, + update_single_hook_precommit_repo, +) def main() -> None: - index, repo = _get_nbstripout_precommit_repo() - if repo is None or index is None: + # cspell:ignore nbconvert showmarkdowntxt + if not CONFIG_PATH.precommit.exists(): return - _update_extra_keys_argument(index, repo) - - -def _get_nbstripout_precommit_repo() -> Tuple[Optional[int], Optional[Repo]]: - config = PrecommitConfig.load() - repo = config.find_repo(__REPO_URL) - index = config.get_repo_index(__REPO_URL) - return index, repo - - -def _update_extra_keys_argument(repo_index: int, repo: Repo) -> None: - """Add an argument to strip additional metadata. - - For more info see https://github.com/kynan/nbstripout#stripping-metadata. - """ - index = repo.get_hook_index(__HOOK_ID) - if index is None: - raise PrecommitError( - f'The following repo is missing hook ID "{__HOOK_ID}": {__REPO_URL}' - ) - expected_args = [ - "--extra-keys", - LiteralScalarString("\n".join(__EXTRA_KEYS_ARGUMENT) + "\n"), - ] - if repo.hooks[index].args == [str(s) for s in expected_args]: + config, _ = load_round_trip_precommit_config() + repo_url = "https://github.com/kynan/nbstripout" + idx_and_repo = find_repo(config, repo_url) + if idx_and_repo is None: return - yaml = create_prettier_round_trip_yaml() - config = yaml.load(CONFIG_PATH.precommit) - repos = config["repos"] - hooks = repos[repo_index]["hooks"][index] - hooks["args"] = expected_args - if repo_index != len(repos): - repos.yaml_set_comment_before_after_key(repo_index + 1, before="\n") - yaml.dump(config, CONFIG_PATH.precommit) + extra_keys_argument = [ + "cell.attachments", + "cell.metadata.code_folding", + "cell.metadata.id", + "cell.metadata.user_expressions", + "metadata.celltoolbar", + "metadata.colab.name", + "metadata.colab.provenance", + "metadata.interpreter", + "metadata.notify_time", + "metadata.toc", + "metadata.toc-autonumbering", + "metadata.toc-showcode", + "metadata.toc-showmarkdowntxt", + "metadata.toc-showtags", + "metadata.varInspector", + "metadata.vscode", + ] + expected_hook = CommentedMap( + repo=repo_url, + hooks=[ + CommentedMap( + id="nbstripout", + args=[ + "--extra-keys", + LiteralScalarString("\n".join(extra_keys_argument) + "\n"), + ], + ) + ], + ) + update_single_hook_precommit_repo(expected_hook) diff --git a/src/repoma/check_dev_files/pyupgrade.py b/src/repoma/check_dev_files/pyupgrade.py index 4cc3af08..c87ced67 100644 --- a/src/repoma/check_dev_files/pyupgrade.py +++ b/src/repoma/check_dev_files/pyupgrade.py @@ -1,85 +1,44 @@ """Install `pyupgrade `_ as a hook.""" +from ruamel.yaml.comments import CommentedMap -from repoma.errors import PrecommitError -from repoma.utilities import CONFIG_PATH, natural_sorting +from repoma.utilities import natural_sorting from repoma.utilities.executor import Executor from repoma.utilities.precommit import ( - Hook, - PrecommitConfig, - asdict, - load_round_trip_precommit_config, + update_precommit_hook, + update_single_hook_precommit_repo, ) from repoma.utilities.setup_cfg import get_supported_python_versions def main() -> None: executor = Executor() - executor(_update_main_pyupgrade_hook) - executor(_update_nbqa_hook) + executor(_update_precommit_repo) + executor(_update_precommit_nbqa_hook) executor.finalize() -def _update_main_pyupgrade_hook() -> None: - repo_url = "https://github.com/asottile/pyupgrade" - hook_id = "pyupgrade" - expected_args = [ - __get_pyupgrade_version_argument(), - ] - precommit_config = PrecommitConfig.load() - repo = precommit_config.find_repo(repo_url) - if repo is None: - raise PrecommitError(f"{CONFIG_PATH.precommit} is missing a hook: {repo_url}") - index = repo.get_hook_index(hook_id) - if index is None: - config, yaml = load_round_trip_precommit_config() - config["repos"].append( - { - "repo": repo_url, - "rev": "v2.29.0", - "hooks": [ - { - "id": hook_id, - "args": expected_args, - } - ], - } - ) - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Added {hook_id} pre-commit hook") - if repo.hooks[index].args == expected_args: - return - config, yaml = load_round_trip_precommit_config() - config["repos"][index]["hooks"]["args"] = expected_args - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Updated args of {hook_id} pre-commit hook") - +def _update_precommit_repo() -> None: + expected_hook = CommentedMap( + repo="https://github.com/asottile/pyupgrade", + hooks=[ + CommentedMap( + id="pyupgrade", + args=[__get_pyupgrade_version_argument()], + ) + ], + ) + update_single_hook_precommit_repo(expected_hook) -def _update_nbqa_hook() -> None: - repo_url = "https://github.com/nbQA-dev/nbQA" - precommit_config = PrecommitConfig.load() - repo = precommit_config.find_repo(repo_url) - if repo is None: - return - hook_id = "nbqa-pyupgrade" - expected = Hook( - hook_id, - args=[__get_pyupgrade_version_argument()], +def _update_precommit_nbqa_hook() -> None: + update_precommit_hook( + repo_url="https://github.com/nbQA-dev/nbQA", + expected_hook=CommentedMap( + id="nbqa-pyupgrade", + args=[__get_pyupgrade_version_argument()], + ), ) - repo_index = precommit_config.get_repo_index(repo_url) - hook_index = repo.get_hook_index(hook_id) - if hook_index is None: - config, yaml = load_round_trip_precommit_config() - config["repos"][repo_index]["hooks"].append(asdict(expected)) - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Added {hook_id} to pre-commit config") - - if repo.hooks[hook_index] != expected: - config, yaml = load_round_trip_precommit_config() - config["repos"][repo_index]["hooks"][hook_index] = asdict(expected) - yaml.dump(config, CONFIG_PATH.precommit) - raise PrecommitError(f"Updated args of {hook_id} pre-commit hook") def __get_pyupgrade_version_argument() -> str: diff --git a/src/repoma/check_dev_files/toml.py b/src/repoma/check_dev_files/toml.py index 02e2f34b..9811aa4c 100644 --- a/src/repoma/check_dev_files/toml.py +++ b/src/repoma/check_dev_files/toml.py @@ -3,16 +3,14 @@ import os import shutil from pathlib import Path -from textwrap import dedent from typing import List, Union -from ruamel.yaml.comments import CommentedSeq +from ruamel.yaml.comments import CommentedMap from repoma.errors import PrecommitError from repoma.utilities import CONFIG_PATH, REPOMA_DIR, vscode from repoma.utilities.executor import Executor -from repoma.utilities.precommit import PrecommitConfig -from repoma.utilities.yaml import create_prettier_round_trip_yaml +from repoma.utilities.precommit import update_single_hook_precommit_repo __INCORRECT_TAPLO_CONFIG_PATHS = [ "taplo.toml", @@ -30,7 +28,7 @@ def main() -> None: executor = Executor() executor(_rename_taplo_config) executor(_update_taplo_config) - executor(_update_precommit_config) + executor(_update_precommit_repo) executor(_update_vscode_extensions) executor.finalize() @@ -58,33 +56,12 @@ def _update_taplo_config() -> None: raise PrecommitError(f"Updated {CONFIG_PATH.taplo} config file") -def _update_precommit_config() -> None: - if not os.path.exists(CONFIG_PATH.precommit): - return - existing_config = PrecommitConfig.load() - repo_url = "https://github.com/ComPWA/mirrors-taplo" - if existing_config.find_repo(repo_url) is not None: - return - yaml = create_prettier_round_trip_yaml() - config = yaml.load(CONFIG_PATH.precommit) - hook_definition = { - "repo": repo_url, - "rev": "v0.8.0", - "hooks": [{"id": "taplo"}], - } - repos: CommentedSeq = config["repos"] - repos.append(hook_definition) - repo_idx = len(repos) - 1 - repos.yaml_set_comment_before_after_key(repo_idx, before="\n") - yaml.dump(config, CONFIG_PATH.precommit) - msg = f""" - Added Taplo TOML formatter as a pre-commit hook. Please run - - pre-commit autoupdate --repo {repo_url} - - to update it to the latest version. - """ - raise PrecommitError(dedent(msg).strip()) +def _update_precommit_repo() -> None: + expected_hook = CommentedMap( + repo="https://github.com/ComPWA/mirrors-taplo", + hooks=[CommentedMap(id="taplo")], + ) + update_single_hook_precommit_repo(expected_hook) def _update_vscode_extensions() -> None: diff --git a/src/repoma/utilities/precommit.py b/src/repoma/utilities/precommit.py index e54c4df9..6a274526 100644 --- a/src/repoma/utilities/precommit.py +++ b/src/repoma/utilities/precommit.py @@ -4,6 +4,7 @@ import os.path import re from pathlib import Path +from textwrap import dedent from typing import Any, List, Optional, Tuple, Type, TypeVar, Union import attrs @@ -11,6 +12,7 @@ from attrs import define from ruamel.yaml import YAML from ruamel.yaml.comments import CommentedMap, CommentedSeq +from ruamel.yaml.scalarstring import DoubleQuotedScalarString, PlainScalarString from repoma.errors import PrecommitError @@ -38,6 +40,120 @@ def find_repo( return None +def update_single_hook_precommit_repo(expected_repo_def: CommentedMap) -> None: + """Update the repo definition in :code:`.pre-commit-config.yaml`. + + If the repository is not yet listed under the :code:`repos` key, a new entry will + be automatically inserted. If the repository exists, but the definition is not the + same as expected, the entry in the YAML config will be updated. + """ + if not CONFIG_PATH.precommit.exists(): + return + config, yaml_parser = load_round_trip_precommit_config() + repos: CommentedSeq = config.get("repos", []) + repo_url: str = expected_repo_def["repo"] + idx_and_repo = find_repo(config, repo_url) + hook_id: str = expected_repo_def["hooks"][0]["id"] + if idx_and_repo is None: + if "rev" not in expected_repo_def: + expected_repo_def.insert(1, "rev", DoubleQuotedScalarString("")) + idx = _determine_expected_repo_index(config, hook_id) + repos.insert(idx, expected_repo_def) + repos.yaml_set_comment_before_after_key( + idx if idx + 1 == len(repos) else idx + 1, + before="\n", + ) + yaml_parser.dump(config, CONFIG_PATH.precommit) + msg = dedent(f""" + Added {hook_id} hook to {CONFIG_PATH.precommit}. Please run + + pre-commit autoupdate --repo {repo_url} + + to update to the latest release of this pre-commit repository. + """).strip() + raise PrecommitError(msg) + idx, existing_hook = idx_and_repo + if not _is_equivalent_repo(existing_hook, expected_repo_def): + existing_rev = existing_hook.get("rev") + if existing_rev is not None: + expected_repo_def.insert(1, "rev", PlainScalarString(existing_rev)) + repos[idx] = expected_repo_def + repos.yaml_set_comment_before_after_key(idx + 1, before="\n") + yaml_parser.dump(config, CONFIG_PATH.precommit) + msg = f"Updated {hook_id} hook in {CONFIG_PATH.precommit}" + raise PrecommitError(msg) + + +def _determine_expected_repo_index(config: CommentedMap, hook_id: str) -> int: + repos: CommentedSeq = config["repos"] + for i, repo_def in enumerate(repos): + hooks: CommentedSeq = repo_def["hooks"] + if len(hooks) != 1: + continue + if hook_id.lower() <= repo_def["hooks"][0]["id"].lower(): + return i + return len(repos) + + +def _is_equivalent_repo(expected: CommentedMap, existing: CommentedMap) -> bool: + def remove_rev(config: CommentedMap) -> dict: + config_copy = dict(config) + config_copy.pop("rev", None) + return config_copy + + return remove_rev(expected) == remove_rev(existing) + + +def update_precommit_hook(repo_url: str, expected_hook: CommentedMap) -> None: + """Update the pre-commit hook definition of a specific pre-commit repo. + + Just like :func:`update_precommit_repo`, this function updates the + :code:`.pre-commit-config.yaml` file, but does this only for a specific hook + definition *within* a pre-commit repository definition. + """ + if not CONFIG_PATH.precommit.exists(): + return + config, yaml_parser = load_round_trip_precommit_config() + idx_and_repo = find_repo(config, repo_url) + if idx_and_repo is None: + return + repo_idx, repo = idx_and_repo + repo_name = repo_url.split("/")[-1] + hooks: CommentedSeq = repo["hooks"] + hook_idx = __find_hook_idx(hooks, expected_hook["id"]) + if hook_idx is None: + hook_idx = __determine_expected_hook_idx(hooks, expected_hook["id"]) + hooks.insert(hook_idx, expected_hook) + if hook_idx == len(hooks) - 1: + repos: CommentedMap = config["repos"] + repos.yaml_set_comment_before_after_key(repo_idx + 1, before="\n") + yaml_parser.dump(config, CONFIG_PATH.precommit) + msg = f"Added {expected_hook['id']!r} to {repo_name} pre-commit config" + raise PrecommitError(msg) + + if hooks[hook_idx] != expected_hook: + hooks[hook_idx] = expected_hook + yaml_parser.dump(config, CONFIG_PATH.precommit) + msg = f"Updated args of {expected_hook['id']!r} {repo_name} pre-commit hook" + raise PrecommitError(msg) + + +def __find_hook_idx(hooks: CommentedSeq, hook_id: str) -> Optional[int]: + msg = "" + for i, hook in enumerate(hooks): + msg += " " + hook["id"] + if hook["id"] == hook_id: + return i + return None + + +def __determine_expected_hook_idx(hooks: CommentedSeq, hook_id: str) -> int: + for i, hook in enumerate(hooks): + if hook["id"] > hook_id: + return i + return len(hooks) + + @define class PrecommitCi: """https://pre-commit.ci/#configuration.""" diff --git a/src/repoma/utilities/pyproject.py b/src/repoma/utilities/pyproject.py new file mode 100644 index 00000000..133658f8 --- /dev/null +++ b/src/repoma/utilities/pyproject.py @@ -0,0 +1,14 @@ +"""Tools for loading and inspecting :code:`pyproject.toml`.""" +from collections import OrderedDict +from typing import Optional + +import toml + +from repoma.utilities import CONFIG_PATH + + +def load_pyproject(content: Optional[str] = None) -> dict: + if content is None: + with open(CONFIG_PATH.pyproject) as stream: + return toml.load(stream, _dict=OrderedDict) + return toml.loads(content, _dict=OrderedDict)