Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions packages/pre_commit_excludes/pre_commit_excludes/args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from argparse import ArgumentParser, ArgumentTypeError
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class SkippedExclude:
"""A pair of hook ID and exclude path that should be skipped from removal."""

hook_id: str
path: Path


def parse_skipped_exclude(value: str) -> SkippedExclude:
try:
hook_id, exclude_path = value.split(":", maxsplit=1)
except ValueError as error:
msg = "expected HOOK_ID:EXCLUDE_PATH"
raise ArgumentTypeError(msg) from error

if not hook_id or not exclude_path:
msg = "hook ID and exclude path must not be empty"
raise ArgumentTypeError(msg)

return SkippedExclude(hook_id, Path(exclude_path))


def create_default_parser(description: str) -> ArgumentParser:
parser = ArgumentParser(description=description)
parser.add_argument(
"config",
type=Path,
help="Path to the .pre-commit-config.yaml that should be cleaned up.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Verbose output for debugging.",
)
parser.add_argument(
"-s",
"--skip-exclude",
type=parse_skipped_exclude,
nargs="+",
default=[],
metavar="HOOK_ID:EXCLUDE_PATH",
help="Skip specific excludes from being removed using HOOK_ID:EXCLUDE_PATH.",
)
hook_group = parser.add_mutually_exclusive_group()
hook_group.add_argument(
"-a",
"--all",
action="store_true",
help="Remove unnecessary excludes from all hooks in the config.",
)
hook_group.add_argument(
"--hook",
type=str,
nargs="+",
help="Remove unnecessary excludes from this specific hook.",
)
return parser
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,10 @@
from ruamel.yaml.scalarstring import LiteralScalarString
from ruamel.yaml.util import load_yaml_guess_indent

from pre_commit_excludes.args import SkippedExclude, create_default_parser
from pre_commit_excludes.hook_utils import Hook, get_hook_configs_from_all_repos, load_config, load_hooks, write_config


@dataclass(frozen=True)
class SkippedExclude:
"""A pair of hook ID and exclude path that should be skipped from removal."""

hook_id: str
path: Path


@dataclass(frozen=True)
class CLITools:
"""Binaries used to find the excludes."""
Expand All @@ -32,20 +25,6 @@ class CLITools:
git: Path


def parse_skipped_exclude(value: str) -> SkippedExclude:
try:
hook_id, exclude_path = value.split(":", maxsplit=1)
except ValueError as error:
msg = "expected HOOK_ID:EXCLUDE_PATH"
raise argparse.ArgumentTypeError(msg) from error

if not hook_id or not exclude_path:
msg = "hook ID and exclude path must not be empty"
raise argparse.ArgumentTypeError(msg)

return SkippedExclude(hook_id, Path(exclude_path))


def get_hooks_to_cleanup(hooks: list[Hook], selected_hooks: list[str] | None) -> list[Hook]:
if selected_hooks is None:
return []
Expand Down Expand Up @@ -181,12 +160,7 @@ def remove_excludes_from_config(config_file: Path, excludes_to_remove: dict[str,


def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"config",
type=Path,
help="Path to the .pre-commit-config.yaml that should be cleaned up.",
)
parser = create_default_parser(__doc__)
parser.add_argument(
"--pre-commit-binary",
type=Path,
Expand All @@ -199,34 +173,6 @@ def parse_arguments() -> argparse.Namespace:
required=True,
help="Path to the git binary used to undo local changes made by running the hooks.",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Verbose output for debugging.",
)
parser.add_argument(
"-s",
"--skip-exclude",
type=parse_skipped_exclude,
nargs="+",
default=[],
metavar="HOOK_ID:EXCLUDE_PATH",
help="Skip specific excludes from being removed using HOOK_ID:EXCLUDE_PATH.",
)
hook_group = parser.add_mutually_exclusive_group()
hook_group.add_argument(
"-a",
"--all",
action="store_true",
help="Remove unnecessary excludes from all hooks in the config.",
)
hook_group.add_argument(
"--hook",
type=str,
nargs="+",
help="Remove unnecessary excludes from this specific hook.",
)
return parser.parse_args()


Expand Down
30 changes: 30 additions & 0 deletions tests/pre_commit_excludes/test_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import argparse
from pathlib import Path

import pytest
from pre_commit_excludes.args import SkippedExclude, parse_skipped_exclude


def test_parse_skipped_exclude_should_return_skipped_exclude() -> None:
assert parse_skipped_exclude("ruff:packages/example/foo.py") == SkippedExclude(
hook_id="ruff",
path=Path("packages/example/foo.py"),
)


def test_parse_skipped_exclude_for_path_with_colon_should_preserve_colon() -> None:
assert parse_skipped_exclude("ruff:packages/example:generated/foo.py") == SkippedExclude(
hook_id="ruff",
path=Path("packages/example:generated/foo.py"),
)


def test_parse_skipped_exclude_for_missing_separator_should_raise_error() -> None:
with pytest.raises(argparse.ArgumentTypeError, match="expected HOOK_ID:EXCLUDE_PATH"):
parse_skipped_exclude("ruff")


@pytest.mark.parametrize("value", [":packages/example/foo.py", "ruff:"])
def test_parse_skipped_exclude_for_empty_value_should_raise_error(value: str) -> None:
with pytest.raises(argparse.ArgumentTypeError, match="hook ID and exclude path must not be empty"):
parse_skipped_exclude(value)
29 changes: 1 addition & 28 deletions tests/pre_commit_excludes/test_remove_unnecessary_excludes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from __future__ import annotations

import argparse
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, call

import pytest
from pre_commit_excludes.hook_utils import Hook, load_config, write_config
from pre_commit_excludes.remove_unnecessary_excludes import (
CLITools,
Expand All @@ -15,41 +13,16 @@
get_files_from_exclude_path,
get_hooks_to_cleanup,
is_exclude_unnecessary,
parse_skipped_exclude,
remove_excludes_from_config,
run_pre_commit,
write_tmp_pre_commit_config_without_excludes,
)

if TYPE_CHECKING:
import pytest
from pyfakefs.fake_filesystem import FakeFilesystem


def test_parse_skipped_exclude_should_return_skipped_exclude() -> None:
assert parse_skipped_exclude("ruff:packages/example/foo.py") == SkippedExclude(
hook_id="ruff",
path=Path("packages/example/foo.py"),
)


def test_parse_skipped_exclude_for_path_with_colon_should_preserve_colon() -> None:
assert parse_skipped_exclude("ruff:packages/example:generated/foo.py") == SkippedExclude(
hook_id="ruff",
path=Path("packages/example:generated/foo.py"),
)


def test_parse_skipped_exclude_for_missing_separator_should_raise_error() -> None:
with pytest.raises(argparse.ArgumentTypeError, match="expected HOOK_ID:EXCLUDE_PATH"):
parse_skipped_exclude("ruff")


@pytest.mark.parametrize("value", [":packages/example/foo.py", "ruff:"])
def test_parse_skipped_exclude_for_empty_value_should_raise_error(value: str) -> None:
with pytest.raises(argparse.ArgumentTypeError, match="hook ID and exclude path must not be empty"):
parse_skipped_exclude(value)


def test_get_hooks_to_cleanup_for_selected_hooks_should_return_matching_hooks() -> None:
check_snake_case_hook = Hook("check-snake-case", [Path("foo.py")])
buildifier_hook = Hook("buildifier", [Path("BUILD.bazel")])
Expand Down