mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 08:14:42 +04:00
Merge branch 'master' of github.com:pre-commit/pre-commit
Conflicts: tests/git_test.py
This commit is contained in:
commit
3b0f03d0e0
11 changed files with 243 additions and 160 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
# Hooks are set up as follows
|
# Hooks are set up as follows
|
||||||
# hooks:
|
|
||||||
# -
|
# -
|
||||||
# id: hook_id
|
# id: hook_id
|
||||||
# name: 'Readable name'
|
# name: 'Readable name'
|
||||||
|
|
@ -15,15 +14,14 @@
|
||||||
# # Optional, defaults to zero
|
# # Optional, defaults to zero
|
||||||
# expected_return_value: 0
|
# expected_return_value: 0
|
||||||
|
|
||||||
hooks:
|
-
|
||||||
-
|
|
||||||
id: my_hook
|
id: my_hook
|
||||||
name: My Simple Hook
|
name: My Simple Hook
|
||||||
description: This is my simple hook that does blah
|
description: This is my simple hook that does blah
|
||||||
entry: my-simple-hook.py
|
entry: my-simple-hook.py
|
||||||
language: python
|
language: python
|
||||||
expected_return_value: 0
|
expected_return_value: 0
|
||||||
-
|
-
|
||||||
id: my_grep_based_hook
|
id: my_grep_based_hook
|
||||||
name: My Bash Based Hook
|
name: My Bash Based Hook
|
||||||
description: This is a hook that uses grep to validate some stuff
|
description: This is a hook that uses grep to validate some stuff
|
||||||
|
|
|
||||||
7
manifest.yaml
Normal file
7
manifest.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
-
|
||||||
|
id: validate_manifest
|
||||||
|
name: Validate Manifest
|
||||||
|
description: This validator validates a pre-commit hooks manifest file
|
||||||
|
entry: validate-manifest
|
||||||
|
language: python
|
||||||
51
pre_commit/clientlib/validate_base.py
Normal file
51
pre_commit/clientlib/validate_base.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
|
||||||
|
import jsonschema
|
||||||
|
import jsonschema.exceptions
|
||||||
|
import os.path
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from pre_commit import git
|
||||||
|
|
||||||
|
|
||||||
|
def get_validator(
|
||||||
|
default_filename,
|
||||||
|
json_schema,
|
||||||
|
exception_type,
|
||||||
|
additional_validation_strategy=lambda obj: None,
|
||||||
|
):
|
||||||
|
"""Returns a function which will validate a yaml file for correctness
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default_filename - Default filename to look for if none is specified
|
||||||
|
json_schema - JSON schema to validate file with
|
||||||
|
exception_type - Error type to raise on failure
|
||||||
|
additional_validation_strategy - Strategy for additional validation of
|
||||||
|
the object read from the file. The function should either raise
|
||||||
|
exception_type on failure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def validate(filename=None):
|
||||||
|
filename = filename or os.path.join(git.get_root(), default_filename)
|
||||||
|
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
raise exception_type('File {0} does not exist'.format(filename))
|
||||||
|
|
||||||
|
file_contents = open(filename, 'r').read()
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = yaml.load(file_contents)
|
||||||
|
except Exception as e:
|
||||||
|
raise exception_type(
|
||||||
|
'File {0} is not a valid yaml file'.format(filename), e,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
jsonschema.validate(obj, json_schema)
|
||||||
|
except jsonschema.exceptions.ValidationError as e:
|
||||||
|
raise exception_type(
|
||||||
|
'File {0} is not a valid file'.format(filename), e,
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_validation_strategy(obj)
|
||||||
|
|
||||||
|
return validate
|
||||||
|
|
@ -2,21 +2,15 @@
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import jsonschema
|
|
||||||
import jsonschema.exceptions
|
|
||||||
import os.path
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
import pre_commit.constants as C
|
import pre_commit.constants as C
|
||||||
|
from pre_commit.clientlib.validate_base import get_validator
|
||||||
|
|
||||||
|
|
||||||
class InvalidManifestError(ValueError): pass
|
class InvalidManifestError(ValueError): pass
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_JSON_SCHEMA = {
|
MANIFEST_JSON_SCHEMA = {
|
||||||
'type': 'object',
|
|
||||||
'properties': {
|
|
||||||
'hooks': {
|
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'minItems': 1,
|
'minItems': 1,
|
||||||
'items': {
|
'items': {
|
||||||
|
|
@ -31,18 +25,11 @@ MANIFEST_JSON_SCHEMA = {
|
||||||
},
|
},
|
||||||
'required': ['id', 'name', 'entry'],
|
'required': ['id', 'name', 'entry'],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
'required': ['hooks'],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_is_valid_manifest(file_contents):
|
def additional_manifest_check(obj):
|
||||||
file_objects = yaml.load(file_contents)
|
for hook_config in obj:
|
||||||
|
|
||||||
jsonschema.validate(file_objects, MANIFEST_JSON_SCHEMA)
|
|
||||||
|
|
||||||
for hook_config in file_objects['hooks']:
|
|
||||||
language = hook_config.get('language')
|
language = hook_config.get('language')
|
||||||
|
|
||||||
if language is not None and not any(
|
if language is not None and not any(
|
||||||
|
|
@ -57,41 +44,33 @@ def check_is_valid_manifest(file_contents):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
validate_manifest = get_validator(
|
||||||
|
C.MANIFEST_FILE,
|
||||||
|
MANIFEST_JSON_SCHEMA,
|
||||||
|
InvalidManifestError,
|
||||||
|
additional_manifest_check,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run(argv):
|
def run(argv):
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--filename',
|
'filename',
|
||||||
required=False, default=None,
|
nargs='?', default=None,
|
||||||
help='Manifest filename. Defaults to {0} at root of git repo'.format(
|
help='Manifest filename. Defaults to {0} at root of git repo'.format(
|
||||||
C.MANIFEST_FILE,
|
C.MANIFEST_FILE,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
if args.filename is None:
|
|
||||||
# TODO: filename = git.get_root() + C.MANIFEST_FILE
|
|
||||||
raise NotImplementedError
|
|
||||||
else:
|
|
||||||
filename = args.filename
|
|
||||||
|
|
||||||
if not os.path.exists(filename):
|
|
||||||
print('File {0} does not exist'.format(filename))
|
|
||||||
return 1
|
|
||||||
|
|
||||||
file_contents = open(filename, 'r').read()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yaml.load(file_contents)
|
validate_manifest(args.filename)
|
||||||
except Exception as e:
|
except InvalidManifestError as e:
|
||||||
print('File {0} is not a valid yaml file'.format(filename))
|
print(e.args[0])
|
||||||
print(str(e))
|
# If we have more than one exception argument print the stringified
|
||||||
return 1
|
# version
|
||||||
|
if len(e.args) > 1:
|
||||||
try:
|
print(str(e.args[1]))
|
||||||
check_is_valid_manifest(file_contents)
|
|
||||||
except (jsonschema.exceptions.ValidationError, InvalidManifestError) as e:
|
|
||||||
print('File {0} is not a valid manifest file'.format(filename))
|
|
||||||
print(str(e))
|
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
|
||||||
PRE_COMMIT_FILE = '.pre-commit-config.yaml'
|
CONFIG_FILE = '.pre-commit-config.yaml'
|
||||||
|
|
||||||
PRE_COMMIT_DIR = '.pre-commit-files'
|
PRE_COMMIT_DIR = '.pre-commit-files'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ class PreCommitProject(object):
|
||||||
with self.in_checkout():
|
with self.in_checkout():
|
||||||
if local.path('setup.py').exists():
|
if local.path('setup.py').exists():
|
||||||
local['virtualenv']['py_env']()
|
local['virtualenv']['py_env']()
|
||||||
local['bash'][local['pip']['install', '.']]
|
local['bash']['-c', 'source py_env/bin/activate && pip install .']()
|
||||||
|
print local.cwd.getpath()
|
||||||
|
|
||||||
def create_repo_in_env(git_repo_path, sha):
|
def create_repo_in_env(git_repo_path, sha):
|
||||||
project = PreCommitProject(git_repo_path, sha)
|
project = PreCommitProject(git_repo_path, sha)
|
||||||
|
|
|
||||||
67
tests/clientlib/validate_base_test.py
Normal file
67
tests/clientlib/validate_base_test.py
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
|
||||||
|
import __builtin__
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
import mock
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pre_commit import git
|
||||||
|
from pre_commit.clientlib.validate_base import get_validator
|
||||||
|
|
||||||
|
|
||||||
|
class AdditionalValidatorError(ValueError): pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def noop_validator():
|
||||||
|
return get_validator('example_manifest.yaml', {}, ValueError)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def array_validator():
|
||||||
|
return get_validator('', {'type': 'array'}, ValueError)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def additional_validator():
|
||||||
|
def raises_always(obj):
|
||||||
|
raise AdditionalValidatorError
|
||||||
|
|
||||||
|
return get_validator(
|
||||||
|
'example_manifest.yaml',
|
||||||
|
{},
|
||||||
|
ValueError,
|
||||||
|
additional_validation_strategy=raises_always,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises_for_non_existent_file(noop_validator):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
noop_validator('file_that_does_not_exist.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises_for_invalid_yaml_file(noop_validator):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
noop_validator('tests/data/non_parseable_yaml_file.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults_to_backup_filename(noop_validator):
|
||||||
|
with mock.patch.object(__builtin__, 'open', side_effect=open) as mock_open:
|
||||||
|
noop_validator()
|
||||||
|
mock_open.assert_called_once_with(
|
||||||
|
os.path.join(git.get_root(), 'example_manifest.yaml'), 'r',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises_for_failing_schema(array_validator):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
array_validator('tests/data/valid_yaml_but_invalid_manifest.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_passes_array_schema(array_validator):
|
||||||
|
array_validator('tests/data/array_yaml_file.yaml')
|
||||||
|
|
||||||
|
|
||||||
|
def test_raises_when_additional_validation_fails(additional_validator):
|
||||||
|
with pytest.raises(AdditionalValidatorError):
|
||||||
|
additional_validator()
|
||||||
|
|
@ -1,86 +1,62 @@
|
||||||
|
|
||||||
import __builtin__
|
|
||||||
import jsonschema
|
import jsonschema
|
||||||
|
import jsonschema.exceptions
|
||||||
import pytest
|
import pytest
|
||||||
import mock
|
|
||||||
|
|
||||||
from pre_commit.clientlib.validate_manifest import check_is_valid_manifest
|
from pre_commit.clientlib.validate_manifest import additional_manifest_check
|
||||||
from pre_commit.clientlib.validate_manifest import InvalidManifestError
|
from pre_commit.clientlib.validate_manifest import InvalidManifestError
|
||||||
|
from pre_commit.clientlib.validate_manifest import MANIFEST_JSON_SCHEMA
|
||||||
from pre_commit.clientlib.validate_manifest import run
|
from pre_commit.clientlib.validate_manifest import run
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def print_mock():
|
|
||||||
with mock.patch.object(__builtin__, 'print', autospec=True) as print_mock_obj:
|
|
||||||
yield print_mock_obj
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_returns_1_for_non_existent_module(print_mock):
|
|
||||||
non_existent_filename = 'file_that_does_not_exist'
|
|
||||||
ret = run(['--filename', non_existent_filename])
|
|
||||||
assert ret == 1
|
|
||||||
print_mock.assert_called_once_with(
|
|
||||||
'File {0} does not exist'.format(non_existent_filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_returns_1_for_non_yaml_file(print_mock):
|
|
||||||
non_parseable_filename = 'tests/data/non_parseable_yaml_file.yaml'
|
|
||||||
ret = run(['--filename', non_parseable_filename])
|
|
||||||
assert ret == 1
|
|
||||||
print_mock.assert_any_call(
|
|
||||||
'File {0} is not a valid yaml file'.format(non_parseable_filename),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_returns_1_for_valid_yaml_file_but_invalid_manifest(print_mock):
|
|
||||||
invalid_manifest = 'tests/data/valid_yaml_but_invalid_manifest.yaml'
|
|
||||||
ret = run(['--filename', invalid_manifest])
|
|
||||||
assert ret == 1
|
|
||||||
print_mock.assert_any_call(
|
|
||||||
'File {0} is not a valid manifest file'.format(invalid_manifest)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_returns_0_for_valid_manifest():
|
def test_returns_0_for_valid_manifest():
|
||||||
valid_manifest = 'example_manifest.yaml'
|
assert run(['example_manifest.yaml']) == 0
|
||||||
ret = run(['--filename', valid_manifest])
|
|
||||||
assert ret == 0
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(('manifest', 'expected_exception_type'), (
|
def test_returns_0_for_our_manifest():
|
||||||
|
assert run([]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_1_for_failing():
|
||||||
|
assert run(['tests/data/valid_yaml_but_invalid_manifest.yaml']) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_additional_manifest_check_raises_for_bad_language():
|
||||||
|
with pytest.raises(InvalidManifestError):
|
||||||
|
additional_manifest_check([{'id': 'foo', 'language': 'not valid'}])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('obj'), (
|
||||||
|
[{}],
|
||||||
|
[{'language': 'python'}],
|
||||||
|
[{'language': 'python>2.6'}],
|
||||||
|
))
|
||||||
|
def test_additional_manifest_check_is_ok_with_missing_language(obj):
|
||||||
|
additional_manifest_check(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_according_to_schema(obj, schema):
|
||||||
|
try:
|
||||||
|
jsonschema.validate(obj, schema)
|
||||||
|
return True
|
||||||
|
except jsonschema.exceptions.ValidationError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(('manifest_obj', 'expected'), (
|
||||||
|
([], False),
|
||||||
|
([{'id': 'a', 'name': 'b', 'entry': 'c'}], True),
|
||||||
(
|
(
|
||||||
"""
|
[{
|
||||||
hooks:
|
'id': 'a',
|
||||||
-
|
'name': 'b',
|
||||||
id: foo
|
'entry': 'c',
|
||||||
entry: foo
|
'language': 'python',
|
||||||
""",
|
'expected_return_value': 0,
|
||||||
jsonschema.exceptions.ValidationError,
|
}],
|
||||||
),
|
True,
|
||||||
(
|
|
||||||
"""
|
|
||||||
hooks:
|
|
||||||
-
|
|
||||||
id: foo
|
|
||||||
name: Foo
|
|
||||||
language: Not a Language lol
|
|
||||||
entry: foo
|
|
||||||
""",
|
|
||||||
InvalidManifestError,
|
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
def test_check_invalid_manifests(manifest, expected_exception_type):
|
def test_is_valid_according_to_schema(manifest_obj, expected):
|
||||||
with pytest.raises(expected_exception_type):
|
ret = is_valid_according_to_schema(manifest_obj, MANIFEST_JSON_SCHEMA)
|
||||||
check_is_valid_manifest(manifest)
|
assert ret is expected
|
||||||
|
|
||||||
|
|
||||||
def test_valid_manifest_is_valid():
|
|
||||||
check_is_valid_manifest("""
|
|
||||||
hooks:
|
|
||||||
-
|
|
||||||
id: foo
|
|
||||||
name: Foo
|
|
||||||
entry: foo
|
|
||||||
language: python>2.6
|
|
||||||
""")
|
|
||||||
10
tests/conftest.py
Normal file
10
tests/conftest.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from plumbum import local
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def empty_git_dir(tmpdir):
|
||||||
|
with local.cwd(tmpdir.strpath):
|
||||||
|
local['git']['init']()
|
||||||
|
yield tmpdir.strpath
|
||||||
2
tests/data/array_yaml_file.yaml
Normal file
2
tests/data/array_yaml_file.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
- foo
|
||||||
|
- bar
|
||||||
|
|
@ -16,14 +16,6 @@ def get_sha(git_repo):
|
||||||
with local.cwd(git_repo):
|
with local.cwd(git_repo):
|
||||||
return (local['git']['log', '--format="%H"'] | local['head']['-n1'])().strip('"\n')
|
return (local['git']['log', '--format="%H"'] | local['head']['-n1'])().strip('"\n')
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
|
||||||
def empty_git_dir(tmpdir):
|
|
||||||
with local.cwd(tmpdir.strpath):
|
|
||||||
local['git']['init']()
|
|
||||||
yield tmpdir.strpath
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.yield_fixture
|
@pytest.yield_fixture
|
||||||
def dummy_git_repo(empty_git_dir):
|
def dummy_git_repo(empty_git_dir):
|
||||||
local['touch']['dummy']()
|
local['touch']['dummy']()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue