diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..36f7a13 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: pyastgrep + name: pyastgrep + description: Search by XPath over AST + entry: pyastgrep + language: python + types: [python] + args: ["--pre-commit-mode", "match_is_error"] diff --git a/docs/usage.rst b/docs/usage.rst index b4072bf..c3ef61e 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -113,6 +113,38 @@ the same code. You’ll also need some understanding of how to write XPath expressions (see links at the bottom), but the examples in the next section should get you started. +Usage as pre-commit hook +======================== + +pyastgrep can be used as pre-commit hook. + +Blocking usage of a function (it's just an example for simplicity, for this exact case +see also banned API functionality in ruff/flake8): + +.. code-block:: + + - repo: https://github.com/spookylukey/pyastgrep + rev: + hooks: + - id: pyastgrep + name: No usage of legacy_function_name + args: ['.//FunctionDef[@name="legacy_function_name"]'] + +Requiring something to present in the files + +.. code-block:: + + - repo: https://github.com/spookylukey/pyastgrep + rev: + hooks: + - id: pyastgrep + name: needed_function_name is added to migration files + args: [ + "--pre-commit-mode", + "non_match_is_error", + './/FunctionDef[@name="needed_function_name"]' + ] + files: 'migrations/.*\.py$' Links diff --git a/src/pyastgrep/cli.py b/src/pyastgrep/cli.py index 9c29b03..871d8ff 100644 --- a/src/pyastgrep/cli.py +++ b/src/pyastgrep/cli.py @@ -21,6 +21,12 @@ from .printer import print_results from .search import search_python_files +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from backports.strenum import StrEnum + + NAME_AND_VERSION = "pyastgrep " + __version__ parser = argparse.ArgumentParser( prog=NAME_AND_VERSION, @@ -36,6 +42,11 @@ def context_parameter(param: str) -> int | StatementContext: return int(param) # Will raise ValueError if invalid, which is handled by argparse +class PreCommitMode(StrEnum): + MATCH_IS_ERROR = "match_is_error" + NON_MATCH_IS_ERROR = "non_match_is_error" + + # Names of arguments: # # We try to match names from ripgrep where the behaviour is basically the same @@ -176,6 +187,15 @@ def context_parameter(param: str) -> int | StatementContext: action="store_true", default=False, ) +parser.add_argument( + "--pre-commit-mode", + help="""Define what exit code should be used for match/non-match cases: + match_is_error - pyastgrep will return non-zero exit code for match + non_match_is_error - pyastgrep will return non-zero exit code if NO match + """, + type=PreCommitMode, + choices=list(PreCommitMode), +) parser.add_argument( "expr", help="XPath search expression\n\n", @@ -279,6 +299,14 @@ def main(sys_args: list[str] | None = None, stdin: BinaryIO | None = None) -> in return ERROR except KeyboardInterrupt: sys.exit(1) + if args.pre_commit_mode: + if errors: + return ERROR + if args.pre_commit_mode == PreCommitMode.MATCH_IS_ERROR and matches: + return ERROR + if args.pre_commit_mode == PreCommitMode.NON_MATCH_IS_ERROR and not matches: + return ERROR + return 0 # Match ripgrep: if errors and not args.quiet: return ERROR