Attempt to add a mechanism for tagging hooks

This commit is contained in:
Josh Abrams 2022-11-12 02:06:55 -05:00
parent 1f59f4cba8
commit 189490a938
8 changed files with 67 additions and 8 deletions

View file

@ -80,6 +80,7 @@ MANIFEST_HOOK_DICT = cfgv.Map(
cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'), cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
cfgv.Optional('require_serial', cfgv.check_bool, False), cfgv.Optional('require_serial', cfgv.check_bool, False),
cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []), cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
cfgv.Optional('tags', cfgv.check_array(cfgv.check_string), []),
cfgv.Optional('verbose', cfgv.check_bool, False), cfgv.Optional('verbose', cfgv.check_bool, False),
) )
MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT) MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)

View file

@ -150,7 +150,7 @@ def _run_single_hook(
) -> tuple[bool, bytes]: ) -> tuple[bool, bytes]:
filenames = classifier.filenames_for_hook(hook) filenames = classifier.filenames_for_hook(hook)
if hook.id in skips or hook.alias in skips: if _hook_is_skipped(skips, hook):
output.write( output.write(
_full_msg( _full_msg(
start=hook.name, start=hook.name,
@ -323,6 +323,25 @@ def _has_unstaged_config(config_file: str) -> bool:
# be explicit, other git errors don't mean it has an unstaged config. # be explicit, other git errors don't mean it has an unstaged config.
return retcode == 1 return retcode == 1
def _hook_should_run(args: argparse.Namespace, hook: Hook) -> bool:
if args.hook_stage not in hook.stages:
return False
if args.tags:
return len(set(hook.tags) & set(args.tags)) > 0
return (
not args.hook
or hook.id == args.hook
or hook.alias == args.hook
)
def _hook_is_skipped(skips: Sequence[str], hook: Hook) -> bool:
return (
hook.id in skips
or hook.alias in skips
or len(set(hook.tags) & set(skips)) > 0
)
def run( def run(
config_file: str, config_file: str,
@ -409,8 +428,7 @@ def run(
hooks = [ hooks = [
hook hook
for hook in all_hooks(config, store) for hook in all_hooks(config, store)
if not args.hook or hook.id == args.hook or hook.alias == args.hook if _hook_should_run(args, hook)
if args.hook_stage in hook.stages
] ]
if args.hook and not hooks: if args.hook and not hooks:
@ -418,12 +436,17 @@ def run(
f'No hook with id `{args.hook}` in stage `{args.hook_stage}`', f'No hook with id `{args.hook}` in stage `{args.hook_stage}`',
) )
return 1 return 1
if args.tags and not hooks:
output.write_line(
f'No hooks with tags matching `{args.tags}` in stage `{args.hook_stage}`'
)
return 1
skips = _get_skips(environ) skips = _get_skips(environ)
to_install = [ to_install = [
hook hook
for hook in hooks for hook in hooks
if hook.id not in skips and hook.alias not in skips if not _hook_is_skipped(skips, hook)
] ]
install_hook_envs(to_install, store) install_hook_envs(to_install, store)

View file

@ -35,6 +35,7 @@ class Hook(NamedTuple):
minimum_pre_commit_version: str minimum_pre_commit_version: str
require_serial: bool require_serial: bool
stages: Sequence[str] stages: Sequence[str]
tags: Sequence[str]
verbose: bool verbose: bool
@property @property

View file

@ -57,7 +57,9 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
def _add_run_options(parser: argparse.ArgumentParser) -> None: def _add_run_options(parser: argparse.ArgumentParser) -> None:
parser.add_argument('hook', nargs='?', help='A single hook-id to run') hooks_mutex_group = parser.add_mutually_exclusive_group(required=False)
hooks_mutex_group.add_argument('hook', nargs='?', help='A single hook-id to run')
hooks_mutex_group.add_argument('--tags', nargs='+', default=[], help='Tag groups to run')
parser.add_argument('--verbose', '-v', action='store_true', default=False) parser.add_argument('--verbose', '-v', action='store_true', default=False)
mutex_group = parser.add_mutually_exclusive_group(required=False) mutex_group = parser.add_mutually_exclusive_group(required=False)
mutex_group.add_argument( mutex_group.add_argument(

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = pre_commit name = pre_commit
version = 2.20.0 version = 2.21.0
description = A framework for managing and maintaining multi-language pre-commit hooks. description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown

View file

@ -3,3 +3,5 @@
entry: bin/hook.sh entry: bin/hook.sh
language: script language: script
files: '' files: ''
tags:
- foo

View file

@ -67,6 +67,7 @@ def run_opts(
color=False, color=False,
verbose=False, verbose=False,
hook=None, hook=None,
tags=(),
remote_branch='', remote_branch='',
local_branch='', local_branch='',
from_ref='', from_ref='',
@ -84,12 +85,14 @@ def run_opts(
): ):
# These are mutually exclusive # These are mutually exclusive
assert not (all_files and files) assert not (all_files and files)
assert not (hook and tags)
return auto_namedtuple( return auto_namedtuple(
all_files=all_files, all_files=all_files,
files=files, files=files,
color=color, color=color,
verbose=verbose, verbose=verbose,
hook=hook, hook=hook,
tags=tags,
remote_branch=remote_branch, remote_branch=remote_branch,
local_branch=local_branch, local_branch=local_branch,
from_ref=from_ref, from_ref=from_ref,

View file

@ -144,12 +144,14 @@ def _do_run(cap_out, store, repo, args, environ={}, config_file=C.CONFIG_FILE):
def _test_run( def _test_run(
cap_out, store, repo, opts, expected_outputs, expected_ret, stage, cap_out, store, repo, opts, expected_outputs, expected_ret, stage,
config_file=C.CONFIG_FILE, config_file=C.CONFIG_FILE, environ_override=None
): ):
if stage: if stage:
stage_a_file() stage_a_file()
if environ_override is None:
environ_override = {}
args = run_opts(**opts) args = run_opts(**opts)
ret, printed = _do_run(cap_out, store, repo, args, config_file=config_file) ret, printed = _do_run(cap_out, store, repo, args, config_file=config_file, environ=environ_override)
assert ret == expected_ret, (ret, expected_ret, printed) assert ret == expected_ret, (ret, expected_ret, printed)
for expected_output_part in expected_outputs: for expected_output_part in expected_outputs:
@ -352,6 +354,7 @@ def test_show_diff_on_failure(
({}, (b'Bash hook', b'Passed'), 0, True), ({}, (b'Bash hook', b'Passed'), 0, True),
({'verbose': True}, (b'foo.py\nHello World',), 0, True), ({'verbose': True}, (b'foo.py\nHello World',), 0, True),
({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True), ({'hook': 'bash_hook'}, (b'Bash hook', b'Passed'), 0, True),
({'tags': ['foo']}, (b'Bash hook', b'Passed'), 0, True),
( (
{'hook': 'nope'}, {'hook': 'nope'},
(b'No hook with id `nope` in stage `commit`',), (b'No hook with id `nope` in stage `commit`',),
@ -364,6 +367,18 @@ def test_show_diff_on_failure(
1, 1,
True, True,
), ),
(
{'tags': ['bar', 'baz']},
(b'No hooks with tags matching `[\'bar\', \'baz\']` in stage `commit`',),
1,
True,
),
(
{'tags': ['bar', 'baz'], 'hook_stage': 'push'},
(b'No hooks with tags matching `[\'bar\', \'baz\']` in stage `push`',),
1,
True,
),
( (
{'all_files': True, 'verbose': True}, {'all_files': True, 'verbose': True},
(b'foo.py',), (b'foo.py',),
@ -618,6 +633,18 @@ def test_skip_aliased_hook(cap_out, store, aliased_repo):
for msg in (b'Bash hook', b'Skipped'): for msg in (b'Bash hook', b'Skipped'):
assert printed.count(msg) == 1 assert printed.count(msg) == 1
def test_skip_tag(cap_out, store, repo_with_passing_hook):
_test_run(
cap_out,
store,
repo_with_passing_hook,
{},
(b'Bash hook', b'Skipped'),
0,
True,
environ_override={'SKIP': 'foo'}
)
def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook): def test_skip_bypasses_installation(cap_out, store, repo_with_passing_hook):
config = { config = {