From e06392134b0e0a6db034d386301cf2b0c685b5e9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Mar 2014 10:17:33 -0700 Subject: [PATCH 1/3] Add a constants directory. --- pre_commit/constants.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pre_commit/constants.py diff --git a/pre_commit/constants.py b/pre_commit/constants.py new file mode 100644 index 00000000..b1de7600 --- /dev/null +++ b/pre_commit/constants.py @@ -0,0 +1,4 @@ + +PRE_COMMIT_FILE = '.pre-commit-config.yaml' + +PRE_COMMIT_DIR = '.pre-commit-files' From 3945f84e3fb695d7dcf76567025f138764f1d8ab Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Mar 2014 12:36:25 -0700 Subject: [PATCH 2/3] Started implementing manifest validating --- example_manifest.yaml | 31 ++++++ pre_commit/clientlib/__init__.py | 0 pre_commit/clientlib/validate_manifest.py | 98 +++++++++++++++++++ pre_commit/constants.py | 8 ++ requirements.txt | 1 + scripts/validate-manifest.py | 8 ++ setup.py | 3 + tests/clientlib/__init__.py | 0 tests/clientlib/validate_manifest_test.py | 86 ++++++++++++++++ tests/data/non_parseable_yaml_file.yaml | 1 + .../data/valid_yaml_but_invalid_manifest.yaml | 1 + 11 files changed, 237 insertions(+) create mode 100644 example_manifest.yaml create mode 100644 pre_commit/clientlib/__init__.py create mode 100644 pre_commit/clientlib/validate_manifest.py create mode 100755 scripts/validate-manifest.py create mode 100644 tests/clientlib/__init__.py create mode 100644 tests/clientlib/validate_manifest_test.py create mode 100644 tests/data/non_parseable_yaml_file.yaml create mode 100644 tests/data/valid_yaml_but_invalid_manifest.yaml 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..46e68e93 --- /dev/null +++ b/pre_commit/clientlib/validate_manifest.py @@ -0,0 +1,98 @@ + +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 index b1de7600..ecf02bcf 100644 --- a/pre_commit/constants.py +++ b/pre_commit/constants.py @@ -2,3 +2,11 @@ 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/requirements.txt b/requirements.txt index c2671ad9..33793d7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ argparse +jsonschema pyyaml simplejson diff --git a/scripts/validate-manifest.py b/scripts/validate-manifest.py new file mode 100755 index 00000000..ce998df6 --- /dev/null +++ b/scripts/validate-manifest.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python + +if __name__ == '__main__': + import sys + + from pre_commit.clientlib.validate_manifest import run + + sys.exit(run(sys.argv[1:])) \ No newline at end of file diff --git a/setup.py b/setup.py index 24ada47a..24ae8eb7 100644 --- a/setup.py +++ b/setup.py @@ -7,9 +7,12 @@ setup( packages=find_packages('.', exclude=('tests*', 'testing*')), install_requires=[ 'argparse', + 'jsonschema', + 'pyyaml', 'simplejson', ], scripts=[ 'scripts/pre-commit.py', + 'scripts/validate-manifest.py', ], ) 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 From 18976b501d9356aff00cf8404b09e67fae55a965 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 13 Mar 2014 13:59:45 -0700 Subject: [PATCH 3/3] Fix entry_points --- pre_commit/clientlib/validate_manifest.py | 1 - pre_commit/entry_points.py | 23 +++++++++++++++++++++++ pre_commit/run.py | 6 +++--- scripts/pre-commit.py | 8 -------- scripts/validate-manifest.py | 8 -------- setup.py | 10 ++++++---- 6 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 pre_commit/entry_points.py delete mode 100755 scripts/pre-commit.py delete mode 100755 scripts/validate-manifest.py diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py index 46e68e93..035114e3 100644 --- a/pre_commit/clientlib/validate_manifest.py +++ b/pre_commit/clientlib/validate_manifest.py @@ -57,7 +57,6 @@ def check_is_valid_manifest(file_contents): ) - def run(argv): parser = argparse.ArgumentParser() parser.add_argument( 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 c281ff72..f8ef0551 100644 --- a/pre_commit/run.py +++ b/pre_commit/run.py @@ -35,8 +35,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) \ No newline at end of file diff --git a/scripts/pre-commit.py b/scripts/pre-commit.py deleted file mode 100755 index 063ad9c2..00000000 --- a/scripts/pre-commit.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -if __name__ == '__main__': - import sys - - from pre_commit.run import run - - sys.exit(run(sys.argv[1:])) \ No newline at end of file diff --git a/scripts/validate-manifest.py b/scripts/validate-manifest.py deleted file mode 100755 index ce998df6..00000000 --- a/scripts/validate-manifest.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python - -if __name__ == '__main__': - import sys - - from pre_commit.clientlib.validate_manifest import run - - sys.exit(run(sys.argv[1:])) \ No newline at end of file diff --git a/setup.py b/setup.py index 9033e75e..5f155857 100644 --- a/setup.py +++ b/setup.py @@ -12,8 +12,10 @@ setup( 'pyyaml', 'simplejson', ], - scripts=[ - 'scripts/pre-commit.py', - 'scripts/validate-manifest.py', - ], + entry_points={ + 'console_scripts': [ + 'pre-commit = pre_commit.entry_points:pre_commit_func', + 'validate-manifest = pre_commit.entry_points:validate_manifest_func', + ], + } )