diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index bda3f76..7fe68e4 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -49,6 +49,12 @@ entry: pretty-format-json language: python files: \.json$ +- id: pretty-format-yaml + name: Pretty format YAML + description: This hook sets a standard for formatting YAML files. + entry: pretty-format-yaml + language: python + files: \.(yaml|yml|eyaml)$ - id: check-merge-conflict name: Check for merge conflicts description: Check for files that contain merge conflict strings. diff --git a/README.md b/README.md index 3b62234..03ec934 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,12 @@ Add this to your `.pre-commit-config.yaml` - `--indent ...` - Control the indentation (either a number for a number of spaces or a string of whitespace). Defaults to 4 spaces. - `--no-sort-keys` - when autofixing, retain the original key ordering (instead of sorting the keys) - `--top-keys comma,separated,keys` - Keys to keep at the top of mappings. +- `pretty-format-yaml` - Checks that all your YAML files are pretty. "Pretty" + here means that keys are sorted and indented. You can configure this with + the following commandline options: + - `--autofix` - automatically format yaml files + - `--default_style`, `--default_flow_style`, `--canonical`, `--indent`, `--width`, `--allow_unicode`, `--line_break`, `--encoding`, `--explicit_start`, `--explicit_end`, `--version`, `--tags` - define how a pretty YAML file looks like. + The parameters are passed as kwargs into yaml.safe_dump method provided by [pyyaml package](http://pyyaml.org/wiki/PyYAMLDocumentation). Our suggestion of pretty YAML file is `-indent=4 --default_flow_style=False`. - `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 bda3f76..7fe68e4 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -49,6 +49,12 @@ entry: pretty-format-json language: python files: \.json$ +- id: pretty-format-yaml + name: Pretty format YAML + description: This hook sets a standard for formatting YAML files. + entry: pretty-format-yaml + language: python + files: \.(yaml|yml|eyaml)$ - 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_yaml.py b/pre_commit_hooks/pretty_format_yaml.py new file mode 100644 index 0000000..3f0fab6 --- /dev/null +++ b/pre_commit_hooks/pretty_format_yaml.py @@ -0,0 +1,127 @@ +from __future__ import print_function + +import argparse +import sys + +import yaml + + +def _get_pretty_format(content, **kwargs): + return yaml.safe_dump(yaml.safe_load(content), **kwargs) + + +def _autofix(filename, new_contents): + print("Fixing file {0}".format(filename)) + with open(filename, 'w') as f: + f.write(new_contents) + + +def filter_argument(accepted_values): + def _validate(s): + try: + return {str(v): v for v in accepted_values}[s] + except: + raise argparse.ArgumentTypeError('Accepted values are: {}'.format(accepted_values)) + return _validate + + +def none_or_boolean_argument(s): + return filter_argument([None, True, False])(s) + + +def pretty_format_yaml(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( + '--default_style', + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--default_flow_style', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--canonical', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--indent', type=int, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--width', type=int, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--allow_unicode', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--line_break', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--encoding', + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--explicit_start', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--explicit_end', type=none_or_boolean_argument, + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--version', + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument( + '--tags', + help='PyYAML dump parameter. More info on http://pyyaml.org/wiki/PyYAMLDocumentation#Dumper' + ) + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + pyyaml_kwargs = { + key: value + for key, value in args._get_kwargs() + if key != 'autofix' and key != 'filenames' + } + + status = 0 + + for yaml_file in args.filenames: + with open(yaml_file) as f: + contents = f.read() + + try: + pretty_contents = _get_pretty_format(contents, **pyyaml_kwargs) + + if contents != pretty_contents: + print("File {0} is not pretty-formatted".format(yaml_file)) + + if args.autofix: + _autofix(yaml_file, pretty_contents) + + status = 1 + + except yaml.YAMLError: + print( + "Input File {0} is not a valid YAML, consider using check-yaml" + .format(yaml_file) + ) + return 1 + + return status + + +if __name__ == '__main__': + sys.exit(pretty_format_yaml(sys.argv[1:])) diff --git a/setup.py b/setup.py index 4abb7a2..45c2786 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ setup( 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', 'no-commit-to-branch = pre_commit_hooks.no_commit_to_branch:main', 'pretty-format-json = pre_commit_hooks.pretty_format_json:pretty_format_json', + 'pretty-format-yaml = pre_commit_hooks.pretty_format_yaml:pretty_format_yaml', '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/yaml_files/not_pretty_formatted_yaml.yaml b/testing/resources/yaml_files/not_pretty_formatted_yaml.yaml new file mode 100644 index 0000000..33841e8 --- /dev/null +++ b/testing/resources/yaml_files/not_pretty_formatted_yaml.yaml @@ -0,0 +1,6 @@ +foo: "bar" +alist: +- 2 +- 34 +- 234 +blah: null diff --git a/testing/resources/yaml_files/pretty_formatted_yaml.yaml b/testing/resources/yaml_files/pretty_formatted_yaml.yaml new file mode 100644 index 0000000..516e4f7 --- /dev/null +++ b/testing/resources/yaml_files/pretty_formatted_yaml.yaml @@ -0,0 +1,3 @@ +alist: [2, 34, 234] +blah: null +foo: bar diff --git a/testing/resources/yaml_files/pretty_formatted_yaml_canonical_True.yaml b/testing/resources/yaml_files/pretty_formatted_yaml_canonical_True.yaml new file mode 100644 index 0000000..e05a3a6 --- /dev/null +++ b/testing/resources/yaml_files/pretty_formatted_yaml_canonical_True.yaml @@ -0,0 +1,19 @@ +--- +!!map { + ? !!str "array" + : !!seq [ + !!int "2", + !!int "34", + !!int "234", + ], + ? !!str "null_items" + : !!null "null", + ? !!str "object" + : !!map { + ? !!str "inner_object" + : !!map { + ? !!str "attribute" + : !!bool "true", + }, + }, +} diff --git a/testing/resources/yaml_files/pretty_formatted_yaml_default_flow_style_False.yaml b/testing/resources/yaml_files/pretty_formatted_yaml_default_flow_style_False.yaml new file mode 100644 index 0000000..3593f08 --- /dev/null +++ b/testing/resources/yaml_files/pretty_formatted_yaml_default_flow_style_False.yaml @@ -0,0 +1,8 @@ +array: +- 2 +- 34 +- 234 +null_items: null +object: + inner_object: + attribute: true diff --git a/testing/resources/yaml_files/pretty_formatted_yaml_default_style_True.yaml b/testing/resources/yaml_files/pretty_formatted_yaml_default_style_True.yaml new file mode 100644 index 0000000..eb1be2d --- /dev/null +++ b/testing/resources/yaml_files/pretty_formatted_yaml_default_style_True.yaml @@ -0,0 +1,8 @@ +"array": +- !!int "2" +- !!int "34" +- !!int "234" +"null_items": !!null "null" +"object": + "inner_object": + "attribute": !!bool "true" diff --git a/testing/resources/yaml_files/pretty_formatted_yaml_indent_4.yaml b/testing/resources/yaml_files/pretty_formatted_yaml_indent_4.yaml new file mode 100644 index 0000000..a60ae5d --- /dev/null +++ b/testing/resources/yaml_files/pretty_formatted_yaml_indent_4.yaml @@ -0,0 +1,4 @@ +array: [2, 34, 234] +null_items: null +object: + inner_object: {attribute: true} diff --git a/tests/pretty_format_yaml_test.py b/tests/pretty_format_yaml_test.py new file mode 100644 index 0000000..edc769a --- /dev/null +++ b/tests/pretty_format_yaml_test.py @@ -0,0 +1,77 @@ +import os +import shutil + +import pytest + +from pre_commit_hooks.pretty_format_yaml import pretty_format_yaml +from testing.util import get_resource_path + + +def get_yaml_resource_path(path): + return get_resource_path(os.path.join('yaml_files', path)) + + +@pytest.mark.parametrize(('filename', 'expected_retval'), ( + ('pretty_formatted_yaml.yaml', 0), + ('not_pretty_formatted_yaml.yaml', 1), +)) +def test_pretty_format_yaml(filename, expected_retval): + ret = pretty_format_yaml([get_yaml_resource_path(filename)]) + assert ret == expected_retval + + +@pytest.mark.parametrize(('filename', 'arguments'), ( + ('pretty_formatted_yaml_default_style_True.yaml', '--default_style=True'), + ('pretty_formatted_yaml_default_flow_style_False.yaml', '--default_flow_style=False'), + ('pretty_formatted_yaml_canonical_True.yaml', '--canonical=True'), + ('pretty_formatted_yaml_indent_4.yaml', '--indent=4'), +)) +def test_pretty_format_yaml_arguments_success(filename, arguments): + assert pretty_format_yaml(arguments.split() + [get_yaml_resource_path(filename)]) == 0 + + +def test_autofix_pretty_format_yaml(tmpdir): + srcfile = tmpdir.join('to_be_yaml_formatted.yaml') + shutil.copyfile( + get_yaml_resource_path('not_pretty_formatted_yaml.yaml'), + srcfile.strpath, + ) + + # now launch the autofix on that file + ret = pretty_format_yaml(['--autofix', srcfile.strpath]) + # it should have formatted it + assert ret == 1 + + # file was formatted (shouldn't trigger linter again) + ret = pretty_format_yaml([srcfile.strpath]) + assert ret == 0 + + +@pytest.mark.parametrize(('argument', 'value'), ( + ('--default_flow_style', 1), + ('--canonical', 'wrong_value'), + ('--indent', 'casual string'), + ('--width', 'casual string'), + ('--allow_unicode', 'no'), + ('--line_break', 'nein'), + ('--explicit_start', 'y'), + ('--explicit_end', 'n'), + +)) +def test_pretty_format_yaml_wrong_arguments(argument, value): + with pytest.raises(SystemExit): + pretty_format_yaml([argument + '=' + str(value), get_yaml_resource_path('pretty_formatted_yaml')]) + + +def test_pretty_format_yaml_invalid_yaml_file(tmpdir): + invalid_yaml_file = tmpdir.join('invalid.yaml') + with open(invalid_yaml_file.strpath, 'w') as invalid_yaml: + invalid_yaml.write(""" +foo: "bar" +alist: +2 +34 +234 +blah: null +""") + assert pretty_format_yaml([invalid_yaml_file.strpath]) == 1