Merge pull request #3092 from pre-commit/minimum-version-first

attempt minimum_pre_commit_version first when parsing configs
This commit is contained in:
Anthony Sottile 2023-12-09 16:04:44 -05:00 committed by GitHub
commit d3fa7f415c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 134 deletions

View file

@ -102,6 +102,13 @@ class StagesMigration(StagesMigrationNoDefault):
MANIFEST_HOOK_DICT = cfgv.Map( MANIFEST_HOOK_DICT = cfgv.Map(
'Hook', 'id', 'Hook', 'id',
# check first in case it uses some newer, incompatible feature
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
cfgv.Required('id', cfgv.check_string), cfgv.Required('id', cfgv.check_string),
cfgv.Required('name', cfgv.check_string), cfgv.Required('name', cfgv.check_string),
cfgv.Required('entry', cfgv.check_string), cfgv.Required('entry', cfgv.check_string),
@ -124,7 +131,6 @@ MANIFEST_HOOK_DICT = cfgv.Map(
cfgv.Optional('description', cfgv.check_string, ''), cfgv.Optional('description', cfgv.check_string, ''),
cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT), cfgv.Optional('language_version', cfgv.check_string, C.DEFAULT),
cfgv.Optional('log_file', cfgv.check_string, ''), cfgv.Optional('log_file', cfgv.check_string, ''),
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),
StagesMigration('stages', []), StagesMigration('stages', []),
cfgv.Optional('verbose', cfgv.check_bool, False), cfgv.Optional('verbose', cfgv.check_bool, False),
@ -345,6 +351,13 @@ DEFAULT_LANGUAGE_VERSION = cfgv.Map(
CONFIG_SCHEMA = cfgv.Map( CONFIG_SCHEMA = cfgv.Map(
'Config', None, 'Config', None,
# check first in case it uses some newer, incompatible feature
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)), cfgv.RequiredRecurse('repos', cfgv.Array(CONFIG_REPO_DICT)),
cfgv.Optional( cfgv.Optional(
'default_install_hook_types', 'default_install_hook_types',
@ -358,11 +371,6 @@ CONFIG_SCHEMA = cfgv.Map(
cfgv.Optional('files', check_string_regex, ''), cfgv.Optional('files', check_string_regex, ''),
cfgv.Optional('exclude', check_string_regex, '^$'), cfgv.Optional('exclude', check_string_regex, '^$'),
cfgv.Optional('fail_fast', cfgv.check_bool, False), cfgv.Optional('fail_fast', cfgv.check_bool, False),
cfgv.Optional(
'minimum_pre_commit_version',
cfgv.check_and(cfgv.check_string, check_min_version),
'0',
),
cfgv.WarnAdditionalKeys( cfgv.WarnAdditionalKeys(
( (
'repos', 'repos',

View file

@ -12,7 +12,6 @@ from pre_commit.all_languages import languages
from pre_commit.clientlib import load_manifest from pre_commit.clientlib import load_manifest
from pre_commit.clientlib import LOCAL from pre_commit.clientlib import LOCAL
from pre_commit.clientlib import META from pre_commit.clientlib import META
from pre_commit.clientlib import parse_version
from pre_commit.hook import Hook from pre_commit.hook import Hook
from pre_commit.lang_base import environment_dir from pre_commit.lang_base import environment_dir
from pre_commit.prefix import Prefix from pre_commit.prefix import Prefix
@ -124,15 +123,6 @@ def _hook(
for dct in rest: for dct in rest:
ret.update(dct) ret.update(dct)
version = ret['minimum_pre_commit_version']
if parse_version(version) > parse_version(C.VERSION):
logger.error(
f'The hook `{ret["id"]}` requires pre-commit version {version} '
f'but version {C.VERSION} is installed. '
f'Perhaps run `pip install --upgrade pre-commit`.',
)
exit(1)
lang = ret['language'] lang = ret['language']
if ret['language_version'] == C.DEFAULT: if ret['language_version'] == C.DEFAULT:
ret['language_version'] = root_config['default_language_version'][lang] ret['language_version'] = root_config['default_language_version'][lang]

View file

@ -40,56 +40,51 @@ def test_check_type_tag_success():
@pytest.mark.parametrize( @pytest.mark.parametrize(
('config_obj', 'expected'), ( 'cfg',
( (
{ {
'repos': [{ 'repos': [{
'repo': 'git@github.com:pre-commit/pre-commit-hooks', 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}], 'hooks': [{'id': 'pyflakes', 'files': '\\.py$'}],
}], }],
}, },
True, {
), 'repos': [{
( 'repo': 'git@github.com:pre-commit/pre-commit-hooks',
{ 'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
'repos': [{ 'hooks': [
'repo': 'git@github.com:pre-commit/pre-commit-hooks', {
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37', 'id': 'pyflakes',
'hooks': [ 'files': '\\.py$',
{ 'args': ['foo', 'bar', 'baz'],
'id': 'pyflakes', },
'files': '\\.py$', ],
'args': ['foo', 'bar', 'baz'], }],
}, },
],
}],
},
True,
),
(
{
'repos': [{
'repo': 'git@github.com:pre-commit/pre-commit-hooks',
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
'hooks': [
{
'id': 'pyflakes',
'files': '\\.py$',
# Exclude pattern must be a string
'exclude': 0,
'args': ['foo', 'bar', 'baz'],
},
],
}],
},
False,
),
), ),
) )
def test_config_valid(config_obj, expected): def test_config_valid(cfg):
ret = is_valid_according_to_schema(config_obj, CONFIG_SCHEMA) assert is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
assert ret is expected
def test_invalid_config_wrong_type():
cfg = {
'repos': [{
'repo': 'git@github.com:pre-commit/pre-commit-hooks',
'rev': 'cd74dc150c142c3be70b24eaf0b02cae9d235f37',
'hooks': [
{
'id': 'pyflakes',
'files': '\\.py$',
# Exclude pattern must be a string
'exclude': 0,
'args': ['foo', 'bar', 'baz'],
},
],
}],
}
assert not is_valid_according_to_schema(cfg, CONFIG_SCHEMA)
def test_local_hooks_with_rev_fails(): def test_local_hooks_with_rev_fails():
@ -198,14 +193,13 @@ def test_warn_mutable_rev_conditional():
), ),
) )
def test_sensible_regex_validators_dont_pass_none(validator_cls): def test_sensible_regex_validators_dont_pass_none(validator_cls):
key = 'files' validator = validator_cls('files', cfgv.check_string)
with pytest.raises(cfgv.ValidationError) as excinfo: with pytest.raises(cfgv.ValidationError) as excinfo:
validator = validator_cls(key, cfgv.check_string) validator.check({'files': None})
validator.check({key: None})
assert str(excinfo.value) == ( assert str(excinfo.value) == (
'\n' '\n'
f'==> At key: {key}' '==> At key: files'
'\n' '\n'
'=====> Expected string got NoneType' '=====> Expected string got NoneType'
) )
@ -298,46 +292,36 @@ def test_validate_optional_sensible_regex_at_top_level(caplog, regex, warning):
@pytest.mark.parametrize( @pytest.mark.parametrize(
('manifest_obj', 'expected'), 'manifest_obj',
( (
( [{
[{ 'id': 'a',
'id': 'a', 'name': 'b',
'name': 'b', 'entry': 'c',
'entry': 'c', 'language': 'python',
'language': 'python', 'files': r'\.py$',
'files': r'\.py$', }],
}], [{
True, 'id': 'a',
), 'name': 'b',
( 'entry': 'c',
[{ 'language': 'python',
'id': 'a', 'language_version': 'python3.4',
'name': 'b', 'files': r'\.py$',
'entry': 'c', }],
'language': 'python', # A regression in 0.13.5: always_run and files are permissible
'language_version': 'python3.4', [{
'files': r'\.py$', 'id': 'a',
}], 'name': 'b',
True, 'entry': 'c',
), 'language': 'python',
( 'files': '',
# A regression in 0.13.5: always_run and files are permissible 'always_run': True,
[{ }],
'id': 'a',
'name': 'b',
'entry': 'c',
'language': 'python',
'files': '',
'always_run': True,
}],
True,
),
), ),
) )
def test_valid_manifests(manifest_obj, expected): def test_valid_manifests(manifest_obj):
ret = is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA) assert is_valid_according_to_schema(manifest_obj, MANIFEST_SCHEMA)
assert ret is expected
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -393,8 +377,39 @@ def test_parse_version():
def test_minimum_pre_commit_version_failing(): def test_minimum_pre_commit_version_failing():
cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
with pytest.raises(cfgv.ValidationError) as excinfo:
cfgv.validate(cfg, CONFIG_SCHEMA)
assert str(excinfo.value) == (
f'\n'
f'==> At Config()\n'
f'==> At key: minimum_pre_commit_version\n'
f'=====> pre-commit version 999 is required but version {C.VERSION} '
f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
)
def test_minimum_pre_commit_version_failing_in_config():
cfg = {'repos': [sample_local_config()]}
cfg['repos'][0]['hooks'][0]['minimum_pre_commit_version'] = '999'
with pytest.raises(cfgv.ValidationError) as excinfo:
cfgv.validate(cfg, CONFIG_SCHEMA)
assert str(excinfo.value) == (
f'\n'
f'==> At Config()\n'
f'==> At key: repos\n'
f"==> At Repository(repo='local')\n"
f'==> At key: hooks\n'
f"==> At Hook(id='do_not_commit')\n"
f'==> At key: minimum_pre_commit_version\n'
f'=====> pre-commit version 999 is required but version {C.VERSION} '
f'is installed. Perhaps run `pip install --upgrade pre-commit`.'
)
def test_minimum_pre_commit_version_failing_before_other_error():
cfg = {'repos': 5, 'minimum_pre_commit_version': '999'}
with pytest.raises(cfgv.ValidationError) as excinfo: with pytest.raises(cfgv.ValidationError) as excinfo:
cfg = {'repos': [], 'minimum_pre_commit_version': '999'}
cfgv.validate(cfg, CONFIG_SCHEMA) cfgv.validate(cfg, CONFIG_SCHEMA)
assert str(excinfo.value) == ( assert str(excinfo.value) == (
f'\n' f'\n'

View file

@ -9,7 +9,6 @@ from unittest import mock
import cfgv import cfgv
import pytest import pytest
import re_assert
import pre_commit.constants as C import pre_commit.constants as C
from pre_commit import lang_base from pre_commit import lang_base
@ -27,7 +26,6 @@ from pre_commit.util import cmd_output
from pre_commit.util import cmd_output_b from pre_commit.util import cmd_output_b
from testing.fixtures import make_config_from_repo from testing.fixtures import make_config_from_repo
from testing.fixtures import make_repo from testing.fixtures import make_repo
from testing.fixtures import modify_manifest
from testing.language_helpers import run_language from testing.language_helpers import run_language
from testing.util import cwd from testing.util import cwd
from testing.util import get_resource_path from testing.util import get_resource_path
@ -433,32 +431,6 @@ def test_hook_id_not_present(tempdir_factory, store, caplog):
) )
def test_too_new_version(tempdir_factory, store, caplog):
path = make_repo(tempdir_factory, 'script_hooks_repo')
with modify_manifest(path) as manifest:
manifest[0]['minimum_pre_commit_version'] = '999.0.0'
config = make_config_from_repo(path)
with pytest.raises(SystemExit):
_get_hook(config, store, 'bash_hook')
_, msg = caplog.messages
pattern = re_assert.Matches(
r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but '
r'version \d+\.\d+\.\d+ is installed. '
r'Perhaps run `pip install --upgrade pre-commit`\.$',
)
pattern.assert_matches(msg)
@pytest.mark.parametrize('version', ('0.1.0', C.VERSION))
def test_versions_ok(tempdir_factory, store, version):
path = make_repo(tempdir_factory, 'script_hooks_repo')
with modify_manifest(path) as manifest:
manifest[0]['minimum_pre_commit_version'] = version
config = make_config_from_repo(path)
# Should succeed
_get_hook(config, store, 'bash_hook')
def test_manifest_hooks(tempdir_factory, store): def test_manifest_hooks(tempdir_factory, store):
path = make_repo(tempdir_factory, 'script_hooks_repo') path = make_repo(tempdir_factory, 'script_hooks_repo')
config = make_config_from_repo(path) config = make_config_from_repo(path)