Skip to content

Commit

Permalink
FEAT: automatically black and cSpell pre-commit hooks (#142)
Browse files Browse the repository at this point in the history
* FIX: insert pre-commit repo between other single-hook repos
* MAINT: extract `load_pyproject()` function
* MAINT: extract `update_precommit_hook()`
* MAINT: extract `update_single_hook_precommit_repo()` function
* MAINT: insert empty `rev` if not provided
* MAINT: sort VSCode settings
  • Loading branch information
redeboer committed Jul 1, 2023
1 parent 5ae6306 commit 6799aaf
Show file tree
Hide file tree
Showing 9 changed files with 266 additions and 289 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
71 changes: 33 additions & 38 deletions src/repoma/check_dev_files/black.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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", {})


Expand Down Expand Up @@ -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("""
Expand All @@ -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)
30 changes: 9 additions & 21 deletions src/repoma/check_dev_files/cspell.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
88 changes: 17 additions & 71 deletions src/repoma/check_dev_files/editorconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,13 @@
<https://github.com/editorconfig-checker/editorconfig-checker.python>`_.
"""

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:
Expand All @@ -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)
Loading

0 comments on commit 6799aaf

Please sign in to comment.