hooks.py 3.2 KB

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