Skip to content

Commit 9db827e

Browse files
committed
Refactor meta hooks
1 parent a0a8fc1 commit 9db827e

File tree

8 files changed

+328
-163
lines changed

8 files changed

+328
-163
lines changed
Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,40 @@
1-
import re
2-
import sys
1+
import argparse
32

43
import pre_commit.constants as C
5-
from pre_commit.clientlib import load_config
4+
from pre_commit.commands.run import _filter_by_include_exclude
5+
from pre_commit.commands.run import _filter_by_types
66
from pre_commit.git import get_all_files
7+
from pre_commit.runner import Runner
78

89

9-
def files_matches_any(filenames, include):
10-
include_re = re.compile(include)
11-
for filename in filenames:
12-
if include_re.search(filename):
13-
return True
14-
return False
10+
def check_all_hooks_match_files(config_file):
11+
runner = Runner.create(config_file)
12+
files = get_all_files()
13+
files_matched = True
1514

15+
for repo in runner.repositories:
16+
for hook_id, hook in repo.hooks:
17+
include, exclude = hook['files'], hook['exclude']
18+
filtered = _filter_by_include_exclude(files, include, exclude)
19+
types, exclude_types = hook['types'], hook['exclude_types']
20+
filtered = _filter_by_types(filtered, types, exclude_types)
21+
if not filtered:
22+
print('{} does not apply to this repository'.format(hook_id))
23+
files_matched = False
24+
25+
return files_matched
1626

17-
def check_files_matches_any(config_file=None):
18-
config = load_config(config_file or C.CONFIG_FILE)
19-
files = get_all_files()
20-
files_not_matched = False
2127

22-
for repo in config['repos']:
23-
for hook in repo['hooks']:
24-
include = hook.get('files', '')
25-
if include and not files_matches_any(files, include):
26-
print(
27-
'The files pattern for {} does not match any files'
28-
.format(hook['id'])
29-
)
30-
files_not_matched = True
28+
def main(argv=None):
29+
parser = argparse.ArgumentParser()
30+
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
31+
args = parser.parse_args(argv)
3132

32-
return files_not_matched
33+
retv = 0
34+
for filename in args.filenames:
35+
retv |= not check_all_hooks_match_files(filename)
36+
return retv
3337

3438

3539
if __name__ == '__main__':
36-
sys.exit(check_files_matches_any())
40+
exit(main())
Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
from __future__ import print_function
2+
3+
import argparse
14
import re
2-
import sys
35

46
import pre_commit.constants as C
57
from pre_commit.clientlib import load_config
@@ -14,28 +16,42 @@ def exclude_matches_any(filenames, include, exclude):
1416
return False
1517

1618

17-
def check_useless_excludes(config_file=None):
18-
config = load_config(config_file or C.CONFIG_FILE)
19+
def check_useless_excludes(config_file):
20+
config = load_config(config_file)
1921
files = get_all_files()
2022
useless_excludes = False
2123

2224
exclude = config.get('exclude')
2325
if exclude != '^$' and not exclude_matches_any(files, '', exclude):
24-
print('The global exclude pattern does not match any files')
26+
print(
27+
'The global exclude pattern {!r} does not match any files'
28+
.format(exclude),
29+
)
2530
useless_excludes = True
2631

2732
for repo in config['repos']:
2833
for hook in repo['hooks']:
2934
include, exclude = hook.get('files', ''), hook.get('exclude')
3035
if exclude and not exclude_matches_any(files, include, exclude):
3136
print(
32-
'The exclude pattern for {} does not match any files'
33-
.format(hook['id'])
37+
'The exclude pattern {!r} for {} does not match any files'
38+
.format(exclude, hook['id']),
3439
)
3540
useless_excludes = True
3641

3742
return useless_excludes
3843

3944

45+
def main(argv=None):
46+
parser = argparse.ArgumentParser()
47+
parser.add_argument('filenames', nargs='*', default=[C.CONFIG_FILE])
48+
args = parser.parse_args(argv)
49+
50+
retv = 0
51+
for filename in args.filenames:
52+
retv |= check_useless_excludes(filename)
53+
return retv
54+
55+
4056
if __name__ == '__main__':
41-
sys.exit(check_useless_excludes())
57+
exit(main())

pre_commit/repository.py

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -252,50 +252,56 @@ def _venvs(self):
252252

253253

254254
class MetaRepository(LocalRepository):
255-
# Note: the hook `entry` is passed through `shlex.split()` by the command
256-
# runner, so to prevent issues with spaces and backslashes (on Windows) it
257-
# must be quoted here.
258-
meta_hooks = {
259-
'check-useless-excludes': {
260-
'name': 'Check for useless excludes',
261-
'files': '.pre-commit-config.yaml',
262-
'language': 'system',
263-
'entry': pipes.quote(sys.executable),
264-
'args': ['-m', 'pre_commit.meta_hooks.check_useless_excludes'],
265-
},
266-
'check-files-matches-any': {
267-
'name': 'Check hooks match any files',
268-
'files': '.pre-commit-config.yaml',
269-
'language': 'system',
270-
'entry': pipes.quote(sys.executable),
271-
'args': ['-m', 'pre_commit.meta_hooks.check_files_matches_any'],
272-
},
273-
}
255+
@cached_property
256+
def manifest_hooks(self):
257+
# The hooks are imported here to prevent circular imports.
258+
from pre_commit.meta_hooks import check_files_matches_any
259+
from pre_commit.meta_hooks import check_useless_excludes
260+
261+
# Note: the hook `entry` is passed through `shlex.split()` by the
262+
# command runner, so to prevent issues with spaces and backslashes
263+
# (on Windows) it must be quoted here.
264+
meta_hooks = [
265+
{
266+
'id': 'check-useless-excludes',
267+
'name': 'Check for useless excludes',
268+
'files': '.pre-commit-config.yaml',
269+
'language': 'system',
270+
'entry': pipes.quote(sys.executable),
271+
'args': ['-m', check_useless_excludes.__name__],
272+
},
273+
{
274+
'id': 'check-files-matches-any',
275+
'name': 'Check hooks match any files',
276+
'files': '.pre-commit-config.yaml',
277+
'language': 'system',
278+
'entry': pipes.quote(sys.executable),
279+
'args': ['-m', check_files_matches_any.__name__],
280+
},
281+
]
282+
283+
return {
284+
hook['id']: apply_defaults(
285+
validate(hook, MANIFEST_HOOK_DICT),
286+
MANIFEST_HOOK_DICT,
287+
)
288+
for hook in meta_hooks
289+
}
274290

275291
@cached_property
276292
def hooks(self):
277293
for hook in self.repo_config['hooks']:
278-
if hook['id'] not in self.meta_hooks:
294+
if hook['id'] not in self.manifest_hooks:
279295
logger.error(
280296
'`{}` is not a valid meta hook. '
281297
'Typo? Perhaps it is introduced in a newer version? '
282-
'Often `pre-commit autoupdate` fixes this.'.format(
283-
hook['id'],
284-
),
298+
'Often `pip install --upgrade pre-commit` fixes this.'
299+
.format(hook['id']),
285300
)
286301
exit(1)
287302

288303
return tuple(
289-
(
290-
hook['id'],
291-
apply_defaults(
292-
validate(
293-
dict(self.meta_hooks[hook['id']], **hook),
294-
MANIFEST_HOOK_DICT,
295-
),
296-
MANIFEST_HOOK_DICT,
297-
),
298-
)
304+
(hook['id'], _hook(self.manifest_hooks[hook['id']], hook))
299305
for hook in self.repo_config['hooks']
300306
)
301307

tests/commands/run_test.py

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -670,104 +670,6 @@ def test_meta_hook_passes(
670670
)
671671

672672

673-
def test_useless_exclude_global(
674-
cap_out, repo_with_passing_hook, mock_out_store_directory,
675-
):
676-
config = OrderedDict((
677-
('exclude', 'foo'),
678-
(
679-
'repos', [
680-
OrderedDict((
681-
('repo', 'meta'),
682-
(
683-
'hooks', (
684-
OrderedDict((
685-
('id', 'check-useless-excludes'),
686-
)),
687-
),
688-
),
689-
)),
690-
],
691-
),
692-
))
693-
add_config_to_repo(repo_with_passing_hook, config)
694-
695-
_test_run(
696-
cap_out,
697-
repo_with_passing_hook,
698-
opts={'all_files': True},
699-
expected_outputs=[
700-
b'Check for useless excludes',
701-
b'The global exclude pattern does not match any files',
702-
],
703-
expected_ret=1,
704-
stage=False,
705-
)
706-
707-
708-
def test_useless_exclude_for_hook(
709-
cap_out, repo_with_passing_hook, mock_out_store_directory,
710-
):
711-
config = OrderedDict((
712-
('repo', 'meta'),
713-
(
714-
'hooks', (
715-
OrderedDict((
716-
('id', 'check-useless-excludes'),
717-
('exclude', 'foo'),
718-
)),
719-
),
720-
),
721-
))
722-
add_config_to_repo(repo_with_passing_hook, config)
723-
724-
_test_run(
725-
cap_out,
726-
repo_with_passing_hook,
727-
opts={'all_files': True},
728-
expected_outputs=[
729-
b'Check for useless excludes',
730-
b'The exclude pattern for check-useless-excludes '
731-
b'does not match any files',
732-
],
733-
expected_ret=1,
734-
stage=False,
735-
)
736-
737-
738-
def test_files_match_any(
739-
cap_out, repo_with_passing_hook, mock_out_store_directory,
740-
):
741-
config = OrderedDict((
742-
('repo', 'meta'),
743-
(
744-
'hooks', (
745-
OrderedDict((
746-
('id', 'check-files-matches-any'),
747-
)),
748-
OrderedDict((
749-
('id', 'check-useless-excludes'),
750-
('files', 'foo'),
751-
)),
752-
),
753-
),
754-
))
755-
add_config_to_repo(repo_with_passing_hook, config)
756-
757-
_test_run(
758-
cap_out,
759-
repo_with_passing_hook,
760-
opts={'all_files': True},
761-
expected_outputs=[
762-
b'Check hooks match any files',
763-
b'The files pattern for check-useless-excludes '
764-
b'does not match any files',
765-
],
766-
expected_ret=1,
767-
stage=False,
768-
)
769-
770-
771673
@pytest.yield_fixture
772674
def modified_config_repo(repo_with_passing_hook):
773675
with modify_config(repo_with_passing_hook, commit=False) as config:

tests/meta_hooks/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)