Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: automatically black and cSpell pre-commit hooks #142

Merged
merged 8 commits into from
Jul 1, 2023
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 @@
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)

Check warning on line 31 in src/repoma/check_dev_files/black.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/black.py#L29-L31

Added lines #L29 - L31 were not covered by tests
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 @@
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(

Check warning on line 92 in src/repoma/check_dev_files/black.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/black.py#L92

Added line #L92 was not covered by tests
repo="https://github.com/psf/black",
hooks=[CommentedMap(id="black")],
)
update_single_hook_precommit_repo(expected_hook)

Check warning on line 96 in src/repoma/check_dev_files/black.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/black.py#L96

Added line #L96 was not covered by tests


hook_id = "nbqa-black"
expected = Hook(
hook_id,
additional_dependencies=["black>=22.1.0"],
def _update_precommit_nbqa_hook() -> None:
update_precommit_hook(

Check warning on line 100 in src/repoma/check_dev_files/black.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/black.py#L100

Added line #L100 was not covered by tests
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

Check warning on line 115 in src/repoma/check_dev_files/black.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/black.py#L110-L115

Added lines #L110 - L115 were not covered by tests
nbqa_config = _load_nbqa_black_config()
if nbqa_config != ["--line-length=85"]:
error_message = dedent("""
Expand All @@ -128,12 +130,5 @@

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 @@
if repo is None:
executor(_remove_configuration)
else:
executor(_check_check_hook_options)
executor(_update_precommit_repo)

Check warning on line 51 in src/repoma/check_dev_files/cspell.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/cspell.py#L51

Added line #L51 was not covered by tests
executor(_fix_config_content)
executor(_sort_config_entries)
executor(add_badge, __BADGE)
Expand Down Expand Up @@ -98,23 +97,12 @@
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(

Check warning on line 101 in src/repoma/check_dev_files/cspell.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/cspell.py#L101

Added line #L101 was not covered by tests
repo=__REPO_URL,
hooks=[CommentedMap(id="cspell")],
)
update_single_hook_precommit_repo(expected_hook)

Check warning on line 105 in src/repoma/check_dev_files/cspell.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/cspell.py#L105

Added line #L105 was not covered by tests


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 _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"""

Check warning on line 23 in src/repoma/check_dev_files/editorconfig.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/editorconfig.py#L23

Added line #L23 was not covered by tests
(?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(

Check warning on line 28 in src/repoma/check_dev_files/editorconfig.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/editorconfig.py#L28

Added line #L28 was not covered by tests
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)

Check warning on line 39 in src/repoma/check_dev_files/editorconfig.py

View check run for this annotation

Codecov / codecov/patch

src/repoma/check_dev_files/editorconfig.py#L39

Added line #L39 was not covered by tests
Loading