diff --git a/README.md b/README.md index 7a919e6..ef110d8 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ Add this to your `.pre-commit-config.yaml` - `name-tests-test` - Assert that files in tests/ end in `_test.py`. - Use `args: ['--django']` to match `test*.py` instead. - `pyflakes` - Run pyflakes on your python files. +- `pretty-format-json` - Checks that all your JSON files are pretty - `requirements-txt-fixer` - Sorts entries in requirements.txt - `trailing-whitespace` - Trims trailing whitespace. - Markdown linebreak trailing spaces preserved for `.md` and`.markdown`; diff --git a/hooks.yaml b/hooks.yaml index 13fef85..facee5f 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -31,6 +31,12 @@ entry: check-json language: python files: \.json$ +- id: pretty-format-json + name: Pretty format JSON + description: This hook sets a standard for formatting JSON files. + entry: pretty-format-json + language: python + files: \.json$ - id: check-merge-conflict name: Check for merge conflicts description: Check for files that contain merge conflict strings. diff --git a/pre_commit_hooks/pretty_format_json.py b/pre_commit_hooks/pretty_format_json.py new file mode 100644 index 0000000..d22dada --- /dev/null +++ b/pre_commit_hooks/pretty_format_json.py @@ -0,0 +1,70 @@ +from __future__ import print_function + +import argparse +import sys + +import simplejson + + +def _get_pretty_format(contents, indent): + return simplejson.dumps( + simplejson.loads(contents), + sort_keys=True, + indent=indent + ) + "\n" # dumps don't end with a newline + + +def _autofix(filename, new_contents): + print("Fixing file {0}".format(filename)) + with open(filename, 'w') as f: + f.write(new_contents) + + +def pretty_format_json(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument( + '--autofix', + action='store_true', + dest='autofix', + help='Automatically fixes encountered not-pretty-formatted files' + ) + parser.add_argument( + '--indent', + type=int, + default=2, + help='Number of indent spaces used to pretty-format files' + ) + + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + status = 0 + + for json_file in args.filenames: + try: + f = open(json_file, 'r') + contents = f.read() + f.close() + + pretty_contents = _get_pretty_format(contents, args.indent) + + if contents != pretty_contents: + print("File {0} is not pretty-formatted".format(json_file)) + + if args.autofix: + _autofix(json_file, pretty_contents) + + status = 1 + + except simplejson.JSONDecodeError: + print( + "Input File {0} is not a valid JSON, consider using check-json" + .format(json_file) + ) + return 1 + + return status + + +if __name__ == '__main__': + sys.exit(pretty_format_json()) diff --git a/setup.py b/setup.py index b24c05e..d630a3d 100644 --- a/setup.py +++ b/setup.py @@ -39,15 +39,16 @@ setup( 'check-added-large-files = pre_commit_hooks.check_added_large_files:main', 'check-case-conflict = pre_commit_hooks.check_case_conflict:main', 'check-docstring-first = pre_commit_hooks.check_docstring_first:main', - 'check-merge-conflict = pre_commit_hooks.check_merge_conflict:detect_merge_conflict', 'check-json = pre_commit_hooks.check_json:check_json', + 'check-merge-conflict = pre_commit_hooks.check_merge_conflict:detect_merge_conflict', 'check-xml = pre_commit_hooks.check_xml:check_xml', 'check-yaml = pre_commit_hooks.check_yaml:check_yaml', 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:debug_statement_hook', 'detect-private-key = pre_commit_hooks.detect_private_key:detect_private_key', + 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', - 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', + 'pretty-format-json = pre_commit_hooks.pretty_format_json:pretty_format_json', 'requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:fix_requirements_txt', 'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:fix_trailing_whitespace', ], diff --git a/testing/resources/not_pretty_formatted_json.json b/testing/resources/not_pretty_formatted_json.json new file mode 100644 index 0000000..6376d59 --- /dev/null +++ b/testing/resources/not_pretty_formatted_json.json @@ -0,0 +1,5 @@ +{ + "foo": "bar", + "alist": [2, 34, 234], + "blah": null +} diff --git a/testing/resources/pretty_formatted_json.json b/testing/resources/pretty_formatted_json.json new file mode 100644 index 0000000..ae9b1b2 --- /dev/null +++ b/testing/resources/pretty_formatted_json.json @@ -0,0 +1,9 @@ +{ + "alist": [ + 2, + 34, + 234 + ], + "blah": null, + "foo": "bar" +} diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py new file mode 100644 index 0000000..87086e7 --- /dev/null +++ b/tests/pretty_format_json_test.py @@ -0,0 +1,40 @@ +import pytest +import tempfile + +from pre_commit_hooks.pretty_format_json import pretty_format_json +from testing.util import get_resource_path + + +@pytest.mark.parametrize(('filename', 'expected_retval'), ( + ('not_pretty_formatted_json.json', 1), + ('pretty_formatted_json.json', 0), +)) +def test_pretty_format_json(filename, expected_retval): + ret = pretty_format_json([get_resource_path(filename)]) + assert ret == expected_retval + + +def test_autofix_pretty_format_json(): + toformat_file = tempfile.NamedTemporaryFile(delete=False, mode='w+') + + # copy our file to format there + model_file = open(get_resource_path('not_pretty_formatted_json.json'), 'r') + model_contents = model_file.read() + model_file.close() + + toformat_file.write(model_contents) + toformat_file.close() + + # now launch the autofix on that file + ret = pretty_format_json(['--autofix', toformat_file.name]) + # it should have formatted it + assert ret == 1 + + # file already good + ret = pretty_format_json([toformat_file.name]) + assert ret == 0 + + +def test_badfile_pretty_format_json(): + ret = pretty_format_json([get_resource_path('ok_yaml.yaml')]) + assert ret == 1