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