diff --git a/example_manifest.yaml b/example_manifest.yaml new file mode 100644 index 00000000..426b8545 --- /dev/null +++ b/example_manifest.yaml @@ -0,0 +1,31 @@ + +# Hooks are set up as follows +# hooks: +# - +# id: hook_id +# name: 'Readable name' +# entry: my_hook_executable +# +# # Optional +# description: 'Longer description of the hook' +# +# # Optional, for now 'python[optional version]', 'ruby #.#.#', 'node' +# language: 'python' +# +# # Optional, defaults to zero +# expected_return_value: 0 + +hooks: + - + id: my_hook + name: My Simple Hook + description: This is my simple hook that does blah + entry: my-simple-hook.py + language: python + expected_return_value: 0 + - + id: my_grep_based_hook + name: My Bash Based Hook + description: This is a hook that uses grep to validate some stuff + entry: ./my_grep_based_hook.sh + expected_return_value: 1 diff --git a/pre_commit/clientlib/__init__.py b/pre_commit/clientlib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py new file mode 100644 index 00000000..035114e3 --- /dev/null +++ b/pre_commit/clientlib/validate_manifest.py @@ -0,0 +1,97 @@ + +from __future__ import print_function + +import argparse +import jsonschema +import jsonschema.exceptions +import os.path +import yaml + +import pre_commit.constants as C + + +class InvalidManifestError(ValueError): pass + + +MANIFEST_JSON_SCHEMA = { + 'type': 'object', + 'properties': { + 'hooks': { + 'type': 'array', + 'minItems': 1, + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'entry': {'type': 'string'}, + 'language': {'type': 'string'}, + 'expected_return_value': {'type': 'number'}, + }, + 'required': ['id', 'name', 'entry'], + }, + }, + }, + 'required': ['hooks'], +} + + +def check_is_valid_manifest(file_contents): + file_objects = yaml.load(file_contents) + + jsonschema.validate(file_objects, MANIFEST_JSON_SCHEMA) + + for hook_config in file_objects['hooks']: + language = hook_config.get('language') + + if language is not None and not any( + language.startswith(lang) for lang in C.SUPPORTED_LANGUAGES + ): + raise InvalidManifestError( + 'Expected language {0} for {1} to start with one of {2!r}'.format( + hook_config['id'], + hook_config['language'], + C.SUPPORTED_LANGUAGES, + ) + ) + + +def run(argv): + parser = argparse.ArgumentParser() + parser.add_argument( + '--filename', + required=False, default=None, + help='Manifest filename. Defaults to {0} at root of git repo'.format( + C.MANIFEST_FILE, + ) + ) + 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: + yaml.load(file_contents) + except Exception as e: + print('File {0} is not a valid yaml file'.format(filename)) + print(str(e)) + return 1 + + try: + 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 0 \ No newline at end of file diff --git a/pre_commit/constants.py b/pre_commit/constants.py new file mode 100644 index 00000000..ecf02bcf --- /dev/null +++ b/pre_commit/constants.py @@ -0,0 +1,12 @@ + +PRE_COMMIT_FILE = '.pre-commit-config.yaml' + +PRE_COMMIT_DIR = '.pre-commit-files' + +MANIFEST_FILE = 'manifest.yaml' + +SUPPORTED_LANGUAGES = [ + 'python', + 'ruby', + 'node', +] \ No newline at end of file diff --git a/pre_commit/entry_points.py b/pre_commit/entry_points.py new file mode 100644 index 00000000..4d508256 --- /dev/null +++ b/pre_commit/entry_points.py @@ -0,0 +1,23 @@ + +import functools + +import pre_commit.clientlib.validate_manifest +import pre_commit.run + + +def make_entry_point(entry_point_func): + """Decorator which turns a function which takes sys.argv[1:] and returns + an integer into an argumentless function which returns an integer. + + Args: + entry_point_func - A function which takes an array representing argv + """ + @functools.wraps(entry_point_func) + def func(): + import sys + return entry_point_func(sys.argv[1:]) + return func + + +pre_commit_func = make_entry_point(pre_commit.run.run) +validate_manifest_func = make_entry_point(pre_commit.clientlib.validate_manifest.run) \ No newline at end of file diff --git a/pre_commit/run.py b/pre_commit/run.py index 5fa1741d..400cbc9d 100644 --- a/pre_commit/run.py +++ b/pre_commit/run.py @@ -34,8 +34,8 @@ def run(argv): args = parser.parse_args(argv) if args.install: - install() + return install() elif args.uninstall: - uninstall() + return uninstall() else: - run_hooks(args) \ No newline at end of file + return run_hooks(args) diff --git a/requirements.txt b/requirements.txt index 3e103960..ccb06e82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ argparse +jsonschema plumbum pyyaml simplejson diff --git a/setup.py b/setup.py index 1f456312..449342ce 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,15 @@ setup( }, install_requires=[ 'argparse', + 'jsonschema', 'plumbum', + 'pyyaml', 'simplejson', ], + entry_points={ + 'console_scripts': [ + 'pre-commit = pre_commit.entry_points:pre_commit_func', + 'validate-manifest = pre_commit.entry_points:validate_manifest_func', + ], + } ) diff --git a/tests/clientlib/__init__.py b/tests/clientlib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py new file mode 100644 index 00000000..7996525d --- /dev/null +++ b/tests/clientlib/validate_manifest_test.py @@ -0,0 +1,86 @@ + +import __builtin__ +import jsonschema +import pytest +import mock + +from pre_commit.clientlib.validate_manifest import check_is_valid_manifest +from pre_commit.clientlib.validate_manifest import InvalidManifestError +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(): + valid_manifest = 'example_manifest.yaml' + ret = run(['--filename', valid_manifest]) + assert ret == 0 + + +@pytest.mark.parametrize(('manifest', 'expected_exception_type'), ( + ( + """ +hooks: + - + id: foo + entry: foo + """, + jsonschema.exceptions.ValidationError, + ), + ( + """ +hooks: + - + id: foo + name: Foo + language: Not a Language lol + entry: foo + """, + InvalidManifestError, + ), +)) +def test_check_invalid_manifests(manifest, expected_exception_type): + with pytest.raises(expected_exception_type): + check_is_valid_manifest(manifest) + + +def test_valid_manifest_is_valid(): + check_is_valid_manifest(""" +hooks: + - + id: foo + name: Foo + entry: foo + language: python>2.6 + """) diff --git a/tests/data/non_parseable_yaml_file.yaml b/tests/data/non_parseable_yaml_file.yaml new file mode 100644 index 00000000..cdadb58f --- /dev/null +++ b/tests/data/non_parseable_yaml_file.yaml @@ -0,0 +1 @@ +foo: " \ No newline at end of file diff --git a/tests/data/valid_yaml_but_invalid_manifest.yaml b/tests/data/valid_yaml_but_invalid_manifest.yaml new file mode 100644 index 00000000..7daacd5d --- /dev/null +++ b/tests/data/valid_yaml_but_invalid_manifest.yaml @@ -0,0 +1 @@ +foo: bar \ No newline at end of file