Skip to content

Commit d3fa7f4

Browse files
authored
Merge pull request #3092 from pre-commit/minimum-version-first
attempt minimum_pre_commit_version first when parsing configs
2 parents 23a2b73 + 047439a commit d3fa7f4

File tree

4 files changed

+119
-134
lines changed

4 files changed

+119
-134
lines changed

pre_commit/clientlib.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ def apply_default(self, dct: dict[str, Any]) -> None:
102102
MANIFEST_HOOK_DICT = cfgv.Map(
103103
'Hook', 'id',
104104

105+
# check first in case it uses some newer, incompatible feature
106+
cfgv.Optional(
107+
'minimum_pre_commit_version',
108+
cfgv.check_and(cfgv.check_string, check_min_version),
109+
'0',
110+
),
111+
105112
cfgv.Required('id', cfgv.check_string),
106113
cfgv.Required('name', cfgv.check_string),
107114
cfgv.Required('entry', cfgv.check_string),
@@ -124,7 +131,6 @@ def apply_default(self, dct: dict[str, Any]) -> None:
124131
cfgv.Optional('description', cfgv.check_string, ''),
125132
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
126133
cfgv.Optional('log_file', cfgv.check_string, ''),
127-
cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
128134
cfgv.Optional('require_serial', cfgv.check_bool, False),
129135
StagesMigration('stages', []),
130136
cfgv.Optional('verbose', cfgv.check_bool, False),
@@ -345,6 +351,13 @@ def check(self, dct: dict[str, Any]) -> None:
345351
CONFIG_SCHEMA = cfgv.Map(
346352
'Config', None,
347353

354+
# check first in case it uses some newer, incompatible feature
355+
cfgv.Optional(
356+
'minimum_pre_commit_version',
357+
cfgv.check_and(cfgv.check_string, check_min_version),
358+
'0',
359+
),
360+
348361
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
349362
cfgv.Optional(
350363
'default_install_hook_types',
@@ -358,11 +371,6 @@ def check(self, dct: dict[str, Any]) -> None:
358371
cfgv.Optional('files', check_string_regex, ''),
359372
cfgv.Optional('exclude', check_string_regex, '^$'),
360373
cfgv.Optional('fail_fast', cfgv.check_bool, False),
361-
cfgv.Optional(
362-
'minimum_pre_commit_version',
363-
cfgv.check_and(cfgv.check_string, check_min_version),
364-
'0',
365-
),
366374
cfgv.WarnAdditionalKeys(
367375
(
368376
'repos',

pre_commit/repository.py

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from pre_commit.clientlib import load_manifest
1313
from pre_commit.clientlib import LOCAL
1414
from pre_commit.clientlib import META
15-
from pre_commit.clientlib import parse_version
1615
from pre_commit.hook import Hook
1716
from pre_commit.lang_base import environment_dir
1817
from pre_commit.prefix import Prefix
@@ -124,15 +123,6 @@ def _hook(
124123
for dct in rest:
125124
ret.update(dct)
126125

127-
version = ret['minimum_pre_commit_version']
128-
if parse_version(version) > parse_version(C.VERSION):
129-
logger.error(
130-
f'The hook `{ret["id"]}` requires pre-commit version {version} '
131-
f'but version {C.VERSION} is installed. '
132-
f'Perhaps run `pip install --upgrade pre-commit`.',
133-
)
134-
exit(1)
135-
136126
lang = ret['language']
137127
if ret['language_version'] == C.DEFAULT:
138128
ret['language_version'] = root_config['default_language_version'][lang]

tests/clientlib_test.py

Lines changed: 105 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -40,56 +40,51 @@ def test_check_type_tag_success():
4040

4141

4242
@pytest.mark.parametrize(
43-
('config_obj', 'expected'), (
44-
(
45-
{
46-
'repos': [{
47-
'repo': '[email protected]:pre-commit/pre-commit-hooks',
48-
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
49-
'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}],
50-
}],
51-
},
52-
True,
53-
),
54-
(
55-
{
56-
'repos': [{
57-
'repo': '[email protected]:pre-commit/pre-commit-hooks',
58-
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
59-
'hooks': [
60-
{
61-
'id': 'pyflakes',
62-
'files': '\\.py$',
63-
'args': ['foo', 'bar', 'baz'],
64-
},
65-
],
66-
}],
67-
},
68-
True,
69-
),
70-
(
71-
{
72-
'repos': [{
73-
'repo': '[email protected]:pre-commit/pre-commit-hooks',
74-
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
75-
'hooks': [
76-
{
77-
'id': 'pyflakes',
78-
'files': '\\.py$',
79-
# Exclude pattern must be a string
80-
'exclude': 0,
81-
'args': ['foo', 'bar', 'baz'],
82-
},
83-
],
84-
}],
85-
},
86-
False,
87-
),
43+
'cfg',
44+
(
45+
{
46+
'repos': [{
47+
'repo': '[email protected]:pre-commit/pre-commit-hooks',
48+
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
49+
'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}],
50+
}],
51+
},
52+
{
53+
'repos': [{
54+
'repo': '[email protected]:pre-commit/pre-commit-hooks',
55+
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
56+
'hooks': [
57+
{
58+
'id': 'pyflakes',
59+
'files': '\\.py$',
60+
'args': ['foo', 'bar', 'baz'],
61+
},
62+
],
63+
}],
64+
},
8865
),
8966
)
90-
def test_config_valid(config_obj, expected):
91-
ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA)
92-
assert ret is expected
67+
def test_config_valid(cfg):
68+
assert is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
69+
70+
71+
def test_invalid_config_wrong_type():
72+
cfg = {
73+
'repos': [{
74+
'repo': '[email protected]:pre-commit/pre-commit-hooks',
75+
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
76+
'hooks': [
77+
{
78+
'id': 'pyflakes',
79+
'files': '\\.py$',
80+
# Exclude pattern must be a string
81+
'exclude': 0,
82+
'args': ['foo', 'bar', 'baz'],
83+
},
84+
],
85+
}],
86+
}
87+
assert not is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
9388

9489

9590
def test_local_hooks_with_rev_fails():
@@ -198,14 +193,13 @@ def test_warn_mutable_rev_conditional():
198193
),
199194
)
200195
def test_sensible_regex_validators_dont_pass_none(validator_cls):
201-
key = 'files'
196+
validator = validator_cls('files', cfgv.check_string)
202197
with pytest.raises(cfgv.ValidationError) as excinfo:
203-
validator = validator_cls(key, cfgv.check_string)
204-
validator.check({key: None})
198+
validator.check({'files': None})
205199

206200
assert str(excinfo.value) == (
207201
'\n'
208-
f'==> At key: {key}'
202+
'==> At key: files'
209203
'\n'
210204
'=====> Expected string got NoneType'
211205
)
@@ -298,46 +292,36 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning):
298292

299293

300294
@pytest.mark.parametrize(
301-
('manifest_obj', 'expected'),
295+
'manifest_obj',
302296
(
303-
(
304-
[{
305-
'id': 'a',
306-
'name': 'b',
307-
'entry': 'c',
308-
'language': 'python',
309-
'files': r'\.py$',
310-
}],
311-
True,
312-
),
313-
(
314-
[{
315-
'id': 'a',
316-
'name': 'b',
317-
'entry': 'c',
318-
'language': 'python',
319-
'language_version': 'python3.4',
320-
'files': r'\.py$',
321-
}],
322-
True,
323-
),
324-
(
325-
# A regression in 0.13.5: always_run and files are permissible
326-
[{
327-
'id': 'a',
328-
'name': 'b',
329-
'entry': 'c',
330-
'language': 'python',
331-
'files': '',
332-
'always_run': True,
333-
}],
334-
True,
335-
),
297+
[{
298+
'id': 'a',
299+
'name': 'b',
300+
'entry': 'c',
301+
'language': 'python',
302+
'files': r'\.py$',
303+
}],
304+
[{
305+
'id': 'a',
306+
'name': 'b',
307+
'entry': 'c',
308+
'language': 'python',
309+
'language_version': 'python3.4',
310+
'files': r'\.py$',
311+
}],
312+
# A regression in 0.13.5: always_run and files are permissible
313+
[{
314+
'id': 'a',
315+
'name': 'b',
316+
'entry': 'c',
317+
'language': 'python',
318+
'files': '',
319+
'always_run': True,
320+
}],
336321
),
337322
)
338-
def test_valid_manifests(manifest_obj, expected):
339-
ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
340-
assert ret is expected
323+
def test_valid_manifests(manifest_obj):
324+
assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
341325

342326

343327
@pytest.mark.parametrize(
@@ -393,8 +377,39 @@ def test_parse_version():
393377

394378

395379
def test_minimum_pre_commit_version_failing():
380+
cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
381+
with pytest.raises(cfgv.ValidationError) as excinfo:
382+
cfgv.validate(cfg, CONFIG_SCHEMA)
383+
assert str(excinfo.value) == (
384+
f'\n'
385+
f'==> At Config()\n'
386+
f'==> At key: minimum_pre_commit_version\n'
387+
f'=====> pre-commit version 999 is required but version {C.VERSION} '
388+
f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
389+
)
390+
391+
392+
def test_minimum_pre_commit_version_failing_in_config():
393+
cfg = {'repos': [sample_local_config()]}
394+
cfg['repos'][0]['hooks'][0]['minimum_pre_commit_version'] = '999'
395+
with pytest.raises(cfgv.ValidationError) as excinfo:
396+
cfgv.validate(cfg, CONFIG_SCHEMA)
397+
assert str(excinfo.value) == (
398+
f'\n'
399+
f'==> At Config()\n'
400+
f'==> At key: repos\n'
401+
f"==> At Repository(repo='local')\n"
402+
f'==> At key: hooks\n'
403+
f"==> At Hook(id='do_not_commit')\n"
404+
f'==> At key: minimum_pre_commit_version\n'
405+
f'=====> pre-commit version 999 is required but version {C.VERSION} '
406+
f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
407+
)
408+
409+
410+
def test_minimum_pre_commit_version_failing_before_other_error():
411+
cfg = {'repos': 5, 'minimum_pre_commit_version': '999'}
396412
with pytest.raises(cfgv.ValidationError) as excinfo:
397-
cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
398413
cfgv.validate(cfg, CONFIG_SCHEMA)
399414
assert str(excinfo.value) == (
400415
f'\n'

tests/repository_test.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import cfgv
1111
import pytest
12-
import re_assert
1312

1413
import pre_commit.constants as C
1514
from pre_commit import lang_base
@@ -27,7 +26,6 @@
2726
from pre_commit.util import cmd_output_b
2827
from testing.fixtures import make_config_from_repo
2928
from testing.fixtures import make_repo
30-
from testing.fixtures import modify_manifest
3129
from testing.language_helpers import run_language
3230
from testing.util import cwd
3331
from testing.util import get_resource_path
@@ -433,32 +431,6 @@ def test_hook_id_not_present(tempdir_factory, store, caplog):
433431
)
434432

435433

436-
def test_too_new_version(tempdir_factory, store, caplog):
437-
path = make_repo(tempdir_factory, 'script_hooks_repo')
438-
with modify_manifest(path) as manifest:
439-
manifest[0]['minimum_pre_commit_version'] = '999.0.0'
440-
config = make_config_from_repo(path)
441-
with pytest.raises(SystemExit):
442-
_get_hook(config, store, 'bash_hook')
443-
_, msg = caplog.messages
444-
pattern = re_assert.Matches(
445-
r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but '
446-
r'version \d+\.\d+\.\d+ is installed. '
447-
r'Perhaps run `pip install --upgrade pre-commit`\.$',
448-
)
449-
pattern.assert_matches(msg)
450-
451-
452-
@pytest.mark.parametrize('version', ('0.1.0', C.VERSION))
453-
def test_versions_ok(tempdir_factory, store, version):
454-
path = make_repo(tempdir_factory, 'script_hooks_repo')
455-
with modify_manifest(path) as manifest:
456-
manifest[0]['minimum_pre_commit_version'] = version
457-
config = make_config_from_repo(path)
458-
# Should succeed
459-
_get_hook(config, store, 'bash_hook')
460-
461-
462434
def test_manifest_hooks(tempdir_factory, store):
463435
path = make_repo(tempdir_factory, 'script_hooks_repo')
464436
config = make_config_from_repo(path)

0 commit comments

Comments
 (0)