diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 7a3b380..8b123dc 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -192,3 +192,9 @@ language: python types: [text] stages: [commit, push, manual] +- id: run-sub-hooks + name: Run Subdirectory Hooks + description: Runs subprocess with alternative pre-commit configuration. + entry: run-sub-hooks + language: python + verbose: true diff --git a/README.md b/README.md index b0ea69b..e5194f0 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ Add this to your `.pre-commit-config.yaml` - `--no-sort-keys` - when autofixing, retain the original key ordering (instead of sorting the keys) - `--top-keys comma,separated,keys` - Keys to keep at the top of mappings. - `requirements-txt-fixer` - Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0` +- `run-sub-hooks` - Runs subprocess with alternative pre-commit configuration. + - `--target` - target directory (doesn't have to be repository root + - `--config ...` - Control +- `requirements-txt-fixer` - Sorts entries in requirements.txt and removes incorrect entry for `pkg-resources==0.0.0` - `sort-simple-yaml` - Sorts simple YAML files which consist only of top-level keys, preserving comments and blocks. diff --git a/pre_commit_hooks/run_sub_hooks.py b/pre_commit_hooks/run_sub_hooks.py new file mode 100644 index 0000000..53946d3 --- /dev/null +++ b/pre_commit_hooks/run_sub_hooks.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +""" +Run pre-commit with different config under pre-commit hook. + +Initial idea & code: +https://github.com/pre-commit/pre-commit/issues/731#issuecomment-376945745 +""" +import argparse +import os +import subprocess +import sys +from typing import List +from typing import Optional +from typing import Sequence + + +def all_files(target): # type: (str) -> List[str] + return sum( + ( + [os.path.join(dirpath, file) for file in filenames] + for (dirpath, dirnames, filenames) in os.walk(target) + ), + [], + ) + + +def main(args=None): # type: (Optional[Sequence[str]]) -> int + parser = argparse.ArgumentParser() + parser.add_argument( + '-t', + '--target', + dest='target', + help='target directory to which pre-commit should be limited', + ) + parser.add_argument( + '-c', + '--config', + dest='config', + default='.pre-commit-config.yaml', + help='config in relation to target directory', + ) + parser.add_argument('--all-files', action='store_true') + options, args = parser.parse_known_args(args) + + def is_default(arg): # type: (str) -> bool + return getattr(options, arg) == parser.get_default(arg) + + if is_default('target') and is_default('config'): + parser.error('`target` or `config` has to be specified') + + precommit_cfg = os.path.join(options.target, options.config) + cmd = ['pre-commit', 'run', '--config', precommit_cfg, '--files'] + + if options.all_files: + if options.target == '.': + args.insert(0, '--all-files') + else: + args.extend(all_files(options.target)) + + return subprocess.call(cmd + args, stdout=sys.stdout, stderr=sys.stderr) + + +if __name__ == '__main__': + exit(main()) diff --git a/setup.cfg b/setup.cfg index 12c2b16..041d070 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ console_scripts = no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main pretty-format-json = pre_commit_hooks.pretty_format_json:main requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:main + run-sub-hooks = pre_commit_hooks.run_sub_hooks:main sort-simple-yaml = pre_commit_hooks.sort_simple_yaml:main trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:main diff --git a/tests/run_sub_hooks_test.py b/tests/run_sub_hooks_test.py new file mode 100644 index 0000000..8cf9794 --- /dev/null +++ b/tests/run_sub_hooks_test.py @@ -0,0 +1,55 @@ +import re + +import pytest + +from pre_commit_hooks.run_sub_hooks import main + + +@pytest.fixture +def temp_sub_dir(temp_git_dir): + """Temporary git subdirectory with pre-commit config inside of it""" + with temp_git_dir.as_cwd(): + sub_dir = temp_git_dir.mkdir('subdir') + sub_dir.join('.pre-commit-config.yaml').write( + """\ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.0.0 + hooks: + - id: trailing-whitespace\ +""" + ) + yield sub_dir + + +INFO_PATTERN = re.compile(r'^\s*(\[INFO\] [^\n]+\n)+', flags=re.M) + + +def test_clean_run(temp_sub_dir, capfd): + assert main(['--target', str(temp_sub_dir), '--all-files']) == 0 + captured = capfd.readouterr() + assert ( # noqa: E501 + INFO_PATTERN.sub('', captured.out) == + """\ +Trim Trailing Whitespace.................................................Passed +""" + ) + assert captured.err == '' + + +def test_only_sub_dir_files_when_all_files(temp_git_dir, temp_sub_dir, capfd): + temp_git_dir.join('file1.yaml').write('some trailing whitespace -> ') + temp_sub_dir.join('file2.yaml').write('some trailing whitespace -> ') + assert main(['--target', str(temp_sub_dir), '--all-files']) == 1 + captured = capfd.readouterr() + assert ( # noqa: E501 + INFO_PATTERN.sub('', captured.out) == + """\ +Trim Trailing Whitespace.................................................Failed +hookid: trailing-whitespace + +Fixing subdir/file2.yaml + +""" + ) + assert captured.err == ''