hooks.py 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """Defines a git hook to allow pre-commit warnings and errors about import order.
  2. usage:
  3. exit_code = git_hook(strict=True|False, modify=True|False)
  4. """
  5. import os
  6. import subprocess # nosec - Needed for hook
  7. from pathlib import Path
  8. from typing import List
  9. from isort import Config, api, exceptions
  10. def get_output(command: List[str]) -> str:
  11. """Run a command and return raw output
  12. :param str command: the command to run
  13. :returns: the stdout output of the command
  14. """
  15. result = subprocess.run(command, stdout=subprocess.PIPE, check=True) # nosec - trusted input
  16. return result.stdout.decode()
  17. def get_lines(command: List[str]) -> List[str]:
  18. """Run a command and return lines of output
  19. :param str command: the command to run
  20. :returns: list of whitespace-stripped lines output by command
  21. """
  22. stdout = get_output(command)
  23. return [line.strip() for line in stdout.splitlines()]
  24. def git_hook(
  25. strict: bool = False, modify: bool = False, lazy: bool = False, settings_file: str = ""
  26. ) -> int:
  27. """Git pre-commit hook to check staged files for isort errors
  28. :param bool strict - if True, return number of errors on exit,
  29. causing the hook to fail. If False, return zero so it will
  30. just act as a warning.
  31. :param bool modify - if True, fix the sources if they are not
  32. sorted properly. If False, only report result without
  33. modifying anything.
  34. :param bool lazy - if True, also check/fix unstaged files.
  35. This is useful if you frequently use ``git commit -a`` for example.
  36. If False, only check/fix the staged files for isort errors.
  37. :param str settings_file - A path to a file to be used as
  38. the configuration file for this run.
  39. When settings_file is the empty string, the configuration file
  40. will be searched starting at the directory containing the first
  41. staged file, if any, and going upward in the directory structure.
  42. :return number of errors if in strict mode, 0 otherwise.
  43. """
  44. # Get list of files modified and staged
  45. diff_cmd = ["git", "diff-index", "--cached", "--name-only", "--diff-filter=ACMRTUXB", "HEAD"]
  46. if lazy:
  47. diff_cmd.remove("--cached")
  48. files_modified = get_lines(diff_cmd)
  49. if not files_modified:
  50. return 0
  51. errors = 0
  52. config = Config(
  53. settings_file=settings_file,
  54. settings_path=os.path.dirname(os.path.abspath(files_modified[0])),
  55. )
  56. for filename in files_modified:
  57. if filename.endswith(".py"):
  58. # Get the staged contents of the file
  59. staged_cmd = ["git", "show", f":{filename}"]
  60. staged_contents = get_output(staged_cmd)
  61. try:
  62. if not api.check_code_string(
  63. staged_contents, file_path=Path(filename), config=config
  64. ):
  65. errors += 1
  66. if modify:
  67. api.sort_file(filename, config=config)
  68. except exceptions.FileSkipped: # pragma: no cover
  69. pass
  70. return errors if strict else 0