| """Defines a git hook to allow pre-commit warnings and errors about import order. |
| |
| usage: |
| exit_code = git_hook(strict=True|False, modify=True|False) |
| """ |
| import os |
| import subprocess # nosec - Needed for hook |
| from pathlib import Path |
| from typing import List |
| |
| from isort import Config, api, exceptions |
| |
| |
| def get_output(command: List[str]) -> str: |
| """Run a command and return raw output |
| |
| :param str command: the command to run |
| :returns: the stdout output of the command |
| """ |
| result = subprocess.run(command, stdout=subprocess.PIPE, check=True) # nosec - trusted input |
| return result.stdout.decode() |
| |
| |
| def get_lines(command: List[str]) -> List[str]: |
| """Run a command and return lines of output |
| |
| :param str command: the command to run |
| :returns: list of whitespace-stripped lines output by command |
| """ |
| stdout = get_output(command) |
| return [line.strip() for line in stdout.splitlines()] |
| |
| |
| def git_hook( |
| strict: bool = False, modify: bool = False, lazy: bool = False, settings_file: str = "" |
| ) -> int: |
| """Git pre-commit hook to check staged files for isort errors |
| |
| :param bool strict - if True, return number of errors on exit, |
| causing the hook to fail. If False, return zero so it will |
| just act as a warning. |
| :param bool modify - if True, fix the sources if they are not |
| sorted properly. If False, only report result without |
| modifying anything. |
| :param bool lazy - if True, also check/fix unstaged files. |
| This is useful if you frequently use ``git commit -a`` for example. |
| If False, only check/fix the staged files for isort errors. |
| :param str settings_file - A path to a file to be used as |
| the configuration file for this run. |
| When settings_file is the empty string, the configuration file |
| will be searched starting at the directory containing the first |
| staged file, if any, and going upward in the directory structure. |
| |
| :return number of errors if in strict mode, 0 otherwise. |
| """ |
| # Get list of files modified and staged |
| diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"] |
| if lazy: |
| diff_cmd.remove("--cached") |
| |
| files_modified = get_lines(diff_cmd) |
| if not files_modified: |
| return 0 |
| |
| errors = 0 |
| config = Config( |
| settings_file=settings_file, |
| settings_path=os.path.dirname(os.path.abspath(files_modified[0])), |
| ) |
| for filename in files_modified: |
| if filename.endswith(".py"): |
| # Get the staged contents of the file |
| staged_cmd = ["git", "show", f":{filename}"] |
| staged_contents = get_output(staged_cmd) |
| |
| try: |
| if not api.check_code_string( |
| staged_contents, file_path=Path(filename), config=config |
| ): |
| errors += 1 |
| if modify: |
| api.sort_file(filename, config=config) |
| except exceptions.FileSkipped: # pragma: no cover |
| pass |
| |
| return errors if strict else 0 |