Skip to content

Commit

Permalink
feat: Check root keys on pre-commit file (e.g.: fail_fast)
Browse files Browse the repository at this point in the history
  • Loading branch information
andreoliwa committed Feb 15, 2019
1 parent 8448fa4 commit 9470aed
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 57 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
fail_fast: true
repos:
- repo: local
hooks:
Expand Down
65 changes: 51 additions & 14 deletions flake8_nitpick/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,41 +426,59 @@ class PreCommitChecker(BaseChecker):
file_name = ".pre-commit-config.yaml"
error_base_number = 330

KEY_REPOS = "repos"
KEY_HOOKS = "hooks"
KEY_REPO = "repo"

def suggest_initial_file(self) -> str:
"""Suggest the initial content for a missing file."""
suggested = self.file_toml.copy()
for repo in suggested.get("repos", []):
repo["hooks"] = yaml.load(repo["hooks"])
for repo in suggested.get(self.KEY_REPOS, []):
repo[self.KEY_HOOKS] = yaml.load(repo[self.KEY_HOOKS])
return yaml.dump(suggested, default_flow_style=False)

def check_rules(self) -> YieldFlake8Error:
"""Check the rules for the pre-commit hooks."""
actual = yaml.load(self.file_path.open()) or {}
if "repos" not in actual:
if self.KEY_REPOS not in actual:
yield self.flake8_error(1, "Missing 'repos' in file")
return

actual_repos: List[dict] = actual["repos"] or []
expected_repos: List[dict] = self.file_toml.get("repos", [])
actual_root = actual.copy()
actual_root.pop(self.KEY_REPOS, None)
expected_root = self.file_toml.copy()
expected_root.pop(self.KEY_REPOS, None)
for diff_type, key, values in dictdiffer.diff(expected_root, actual_root):
if diff_type == dictdiffer.REMOVE:
yield from self.show_missing_keys(key, values)
elif diff_type == dictdiffer.CHANGE:
yield from self.compare_different_keys(key, values[0], values[1])

yield from self.check_repos(actual)

def check_repos(self, actual: Dict[str, Any]):
"""Check the repositories configured in pre-commit."""
actual_repos: List[dict] = actual[self.KEY_REPOS] or []
expected_repos: List[dict] = self.file_toml.get(self.KEY_REPOS, [])
for index, expected_repo_dict in enumerate(expected_repos):
repo_name = expected_repo_dict.get("repo")
repo_name = expected_repo_dict.get(self.KEY_REPO)
if not repo_name:
yield self.flake8_error(2, f"Style file is missing 'repo' key in repo #{index}")
yield self.flake8_error(2, f"Style file is missing {self.KEY_REPO!r} key in repo #{index}")
continue

actual_repo_dict = find_object_by_key(actual_repos, "repo", repo_name)
actual_repo_dict = find_object_by_key(actual_repos, self.KEY_REPO, repo_name)
if not actual_repo_dict:
yield self.flake8_error(3, f"Repo {repo_name!r} does not exist under 'repos'")
yield self.flake8_error(3, f"Repo {repo_name!r} does not exist under {self.KEY_REPOS!r}")
continue

if "hooks" not in actual_repo_dict:
yield self.flake8_error(4, f"Missing 'hooks' in repo {repo_name!r}")
if self.KEY_HOOKS not in actual_repo_dict:
yield self.flake8_error(4, f"Missing {self.KEY_HOOKS!r} in repo {repo_name!r}")
continue

actual_hooks = actual_repo_dict.get("hooks") or []
yaml_expected_hooks = expected_repo_dict.get("hooks")
actual_hooks = actual_repo_dict.get(self.KEY_HOOKS) or []
yaml_expected_hooks = expected_repo_dict.get(self.KEY_HOOKS)
if not yaml_expected_hooks:
yield self.flake8_error(5, f"Style file is missing 'hooks' in repo {repo_name!r}")
yield self.flake8_error(5, f"Style file is missing {self.KEY_HOOKS!r} in repo {repo_name!r}")
continue

expected_hooks: List[dict] = yaml.load(yaml_expected_hooks)
Expand All @@ -475,6 +493,25 @@ def check_rules(self) -> YieldFlake8Error:
yield self.flake8_error(7, f"Missing hook with id {hook_id!r}:\n{expected_yaml}")
continue

def show_missing_keys(self, key, values: List[Tuple[str, Any]]):
"""Show the keys that are not present in a section."""
missing = dict(values)
output = yaml.dump(missing, default_flow_style=False)
yield self.flake8_error(8, f"Missing keys:\n{output}")

def compare_different_keys(self, key, raw_expected: Any, raw_actual: Any):
"""Compare different keys."""
if isinstance(raw_actual, (int, float, bool)) or isinstance(raw_expected, (int, float, bool)):
# A boolean "True" or "true" might have the same effect on YAML.
actual = str(raw_actual).lower()
expected = str(raw_expected).lower()
else:
actual = raw_actual
expected = raw_expected
if actual != expected:
example = yaml.dump({key: raw_expected}, default_flow_style=False)
yield self.flake8_error(9, f"Expected value {raw_expected!r} in key, got {raw_actual!r}\n{example}")

@staticmethod
def format_hook(expected_dict: dict) -> str:
"""Format the hook so it's easy to copy and paste it to the .yaml file: ID goes first, indent with spaces."""
Expand Down
3 changes: 3 additions & 0 deletions nitpick-style.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ warn_unused_ignores = true

# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
["pre-commit-config.yaml"]
fail_fast = true

[["pre-commit-config.yaml".repos]]
repo = "local"
hooks = """
Expand Down
5 changes: 5 additions & 0 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
NITPICK_STYLE_TOML,
Flake8Error,
NitpickChecker,
PreCommitChecker,
PyProjectTomlChecker,
SetupCfgChecker,
)
Expand Down Expand Up @@ -83,6 +84,10 @@ def pyproject_toml(self, file_contents: str):
"""Save pyproject.toml."""
return self.save_file(PyProjectTomlChecker.file_name, file_contents)

def pre_commit(self, file_contents: str):
"""Save .pre-commit-config.yaml."""
return self.save_file(PreCommitChecker.file_name, file_contents)

def assert_errors_contain(self, error: str) -> None:
"""Assert the error is in the error set."""
if error in self.errors:
Expand Down
43 changes: 0 additions & 43 deletions tests/test_missing.py

This file was deleted.

91 changes: 91 additions & 0 deletions tests/test_pre_commit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""Pre-commit tests."""
from tests.helpers import ProjectMock


def test_missing_pre_commit_config_yaml(request):
"""Suggest initial contents for missing .pre-commit-config.yaml."""
project = (
ProjectMock(request)
.style(
'''
[["pre-commit-config.yaml".repos]]
repo = "local"
hooks = """
- id: whatever
any: valid
yaml: here
- id: blargh
note: only the id is verified
"""
'''
)
.lint()
)
project.assert_errors_contain(
"""NIP331 File: .pre-commit-config.yaml: Missing file. Suggested initial content:
repos:
- hooks:
- any: valid
id: whatever
yaml: here
- id: blargh
note: only the id is verified
repo: local
"""
)


def test_root_values_on_missing_file(request):
"""Test values on the root of the config file when it's missing."""
project = (
ProjectMock(request)
.style(
"""
["pre-commit-config.yaml"]
fail_fast = true
whatever = "1"
"""
)
.lint()
)
project.assert_errors_contain(
"""NIP331 File: .pre-commit-config.yaml: Missing file. Suggested initial content:
fail_fast: true
whatever: '1'
"""
)


def test_root_values_on_existing_file(request):
"""Test values on the root of the config file when there is a file."""
project = (
ProjectMock(request)
.style(
"""
["pre-commit-config.yaml"]
fail_fast = true
blabla = "what"
something = true
"""
)
.pre_commit(
"""
repos:
- hooks:
- id: whatever
something: false
"""
)
.lint()
)
project.assert_errors_contain(
"""NIP338 File: .pre-commit-config.yaml: Missing keys:
blabla: what
fail_fast: true
"""
)
project.assert_errors_contain(
"""NIP339 File: .pre-commit-config.yaml: Expected value True in key, got False
something: true
"""
)
10 changes: 10 additions & 0 deletions tests/test_pyproject_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""pyproject.toml tests."""
from flake8_nitpick import PyProjectTomlChecker
from tests.helpers import ProjectMock


def test_missing_pyproject_toml(request):
"""Suggest poetry init when pyproject.toml does not exist."""
assert ProjectMock(request, pyproject_toml=False).lint().errors == {
f"NIP201 {PyProjectTomlChecker.file_name} does not exist. Run 'poetry init' to create one."
}

0 comments on commit 9470aed

Please sign in to comment.