diff --git a/packages/pre_commit_excludes/pre_commit_excludes/args.py b/packages/pre_commit_excludes/pre_commit_excludes/args.py new file mode 100644 index 0000000..814c99c --- /dev/null +++ b/packages/pre_commit_excludes/pre_commit_excludes/args.py @@ -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 diff --git a/packages/pre_commit_excludes/pre_commit_excludes/remove_unnecessary_excludes.py b/packages/pre_commit_excludes/pre_commit_excludes/remove_unnecessary_excludes.py index 0ebc45e..5480ea4 100644 --- a/packages/pre_commit_excludes/pre_commit_excludes/remove_unnecessary_excludes.py +++ b/packages/pre_commit_excludes/pre_commit_excludes/remove_unnecessary_excludes.py @@ -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.""" @@ -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 [] @@ -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, @@ -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() diff --git a/tests/pre_commit_excludes/test_args.py b/tests/pre_commit_excludes/test_args.py new file mode 100644 index 0000000..c4e3d2c --- /dev/null +++ b/tests/pre_commit_excludes/test_args.py @@ -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) diff --git a/tests/pre_commit_excludes/test_remove_unnecessary_excludes.py b/tests/pre_commit_excludes/test_remove_unnecessary_excludes.py index 55075df..a4f90fc 100644 --- a/tests/pre_commit_excludes/test_remove_unnecessary_excludes.py +++ b/tests/pre_commit_excludes/test_remove_unnecessary_excludes.py @@ -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, @@ -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")])