From 9425c5d6b5b8c7abd62fff140a754af1452f1209 Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Fri, 23 Jun 2017 00:33:13 -0700 Subject: [PATCH 1/7] First commit of file-contents-sorter precommit hook --- .pre-commit-hooks.yaml | 6 +++ pre_commit_hooks/file_contents_sorter.py | 57 ++++++++++++++++++++++++ requirements-dev.txt | 1 + setup.py | 1 + tests/file_contents_sorter_test.py | 54 ++++++++++++++++++++++ 5 files changed, 119 insertions(+) create mode 100644 pre_commit_hooks/file_contents_sorter.py create mode 100644 tests/file_contents_sorter_test.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index bda3f76..d501d50 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -105,6 +105,12 @@ entry: end-of-file-fixer language: python files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ +- id: file-contents-sorter + name: File Contents Sorter + description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. + entry: file-contents-sorter + language: python + files: '' - id: fix-encoding-pragma name: Fix python encoding pragma language: python diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py new file mode 100644 index 0000000..06c6d3a --- /dev/null +++ b/pre_commit_hooks/file_contents_sorter.py @@ -0,0 +1,57 @@ +""" +A very simple pre-commit hook that, when passed one or more filenames +as arguments, will sort the lines in those files. + +An example use case for this: you have a deploy-whitelist.txt file +in a repo that contains a list of filenames that is used to specify +files to be included in a docker container. This file has one filename +per line. Various users are adding/removing lines from this file; using +this hook on that file should reduce the instances of git merge +conflicts and keep the file nicely ordered. +""" +from __future__ import print_function + +import argparse + +PASS = 0 +FAIL = 1 + + +def sort_file_contents(f): + before = [line for line in f] + after = sorted(before) + + before_string = b''.join(before) + after_string = b''.join(after) + + if before_string == after_string: + return PASS + else: + f.seek(0) + f.write(after_string) + f.truncate() + return FAIL + + +def parse_commandline_input(argv): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='+', help='Files to sort') + args = parser.parse_args(argv) + return args + + +def main(argv=None): + args = parse_commandline_input(argv) + + retv = PASS + + for arg in args.filenames: + with open(arg, 'rb+') as file_obj: + ret_for_file = sort_file_contents(file_obj) + + if ret_for_file: + print('Sorting {}'.format(arg)) + + retv |= ret_for_file + + return retv diff --git a/requirements-dev.txt b/requirements-dev.txt index 2922ef5..4070e66 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,6 +2,7 @@ coverage flake8 +ipdb mock pre-commit pytest diff --git a/setup.py b/setup.py index 4abb7a2..3f761f6 100644 --- a/setup.py +++ b/setup.py @@ -49,6 +49,7 @@ setup( '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', + 'file-contents-sorter = pre_commit_hooks.file_contents_sorter:main', 'fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main', 'forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py new file mode 100644 index 0000000..a8fb4c8 --- /dev/null +++ b/tests/file_contents_sorter_test.py @@ -0,0 +1,54 @@ +from argparse import ArgumentError + +import pytest + +from pre_commit_hooks.file_contents_sorter import FAIL +from pre_commit_hooks.file_contents_sorter import main +from pre_commit_hooks.file_contents_sorter import parse_commandline_input +from pre_commit_hooks.file_contents_sorter import PASS +from pre_commit_hooks.file_contents_sorter import sort_file_contents + + +def _n(*strs): + return b'\n'.join(strs) + '\n' + + +# Input, expected return value, expected output +TESTS = ( + (b'', PASS, b''), + (_n('lonesome'), PASS, _n('lonesome')), + (b'missing_newline', PASS, b'missing_newline'), + (_n('alpha', 'beta'), PASS, _n('alpha', 'beta')), + (_n('beta', 'alpha'), FAIL, _n('alpha', 'beta')), + (_n('C', 'c'), PASS, _n('C', 'c')), + (_n('c', 'C'), FAIL, _n('C', 'c')), + (_n('mag ical ', ' tre vor'), FAIL, _n(' tre vor', 'mag ical ')), + (_n('@', '-', '_', '#'), FAIL, _n('#', '-', '@', '_')), +) + + +@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS) +def test_integration(input_s, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write_binary(input_s) + + output_retval = main([path.strpath]) + + assert path.read_binary() == output + assert output_retval == expected_retval + + +def test_parse_commandline_input_errors_without_args(): + with pytest.raises(SystemExit): + parse_commandline_input([]) + +@pytest.mark.parametrize( + ('filename_list'), + ( + ['filename1'], + ['filename1', 'filename2'], + ) +) +def test_parse_commandline_input_success(filename_list): + args = parse_commandline_input(filename_list) + assert args.filenames == filename_list \ No newline at end of file From 8b41c575db4377b500aba94a2918bbe74a163108 Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Fri, 23 Jun 2017 10:44:10 -0700 Subject: [PATCH 2/7] cp .pre-commit-hooks.yaml hooks.yaml --- .gitignore | 1 + hooks.yaml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 2626934..6fdf044 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.iml *.py[co] .*.sw[a-z] +.cache .coverage .idea .project diff --git a/hooks.yaml b/hooks.yaml index bda3f76..d501d50 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -105,6 +105,12 @@ entry: end-of-file-fixer language: python files: \.(asciidoc|adoc|coffee|cpp|css|c|ejs|erb|groovy|h|haml|hh|hpp|hxx|html|in|j2|jade|json|js|less|markdown|md|ml|mli|pp|py|rb|rs|R|scala|scss|sh|slim|tex|tmpl|ts|txt|yaml|yml)$ +- id: file-contents-sorter + name: File Contents Sorter + description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. + entry: file-contents-sorter + language: python + files: '' - id: fix-encoding-pragma name: Fix python encoding pragma language: python From 4af74511548948f9a45850c9142ee23661aef371 Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Fri, 23 Jun 2017 11:32:05 -0700 Subject: [PATCH 3/7] Update README.md about file-contents-sorter --- README.md | 1 + tests/file_contents_sorter_test.py | 26 ++++++++++++-------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3b62234..894bd83 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ Add this to your `.pre-commit-config.yaml` with single quoted strings. - `end-of-file-fixer` - Makes sure files end in a newline and only a newline. - `fix-encoding-pragma` - Add `# -*- coding: utf-8 -*-` to the top of python files. +- `file-contents-sorter` - Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input to it. - To remove the coding pragma pass `--remove` (useful in a python3-only codebase) - `flake8` - Run flake8 on your python files. - `forbid-new-submodules` - Prevent addition of new git submodules. diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index a8fb4c8..4e65629 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -1,29 +1,26 @@ -from argparse import ArgumentError - import pytest from pre_commit_hooks.file_contents_sorter import FAIL from pre_commit_hooks.file_contents_sorter import main from pre_commit_hooks.file_contents_sorter import parse_commandline_input from pre_commit_hooks.file_contents_sorter import PASS -from pre_commit_hooks.file_contents_sorter import sort_file_contents def _n(*strs): - return b'\n'.join(strs) + '\n' + return b'\n'.join(strs) + b'\n' # Input, expected return value, expected output TESTS = ( (b'', PASS, b''), - (_n('lonesome'), PASS, _n('lonesome')), + (_n(b'lonesome'), PASS, _n(b'lonesome')), (b'missing_newline', PASS, b'missing_newline'), - (_n('alpha', 'beta'), PASS, _n('alpha', 'beta')), - (_n('beta', 'alpha'), FAIL, _n('alpha', 'beta')), - (_n('C', 'c'), PASS, _n('C', 'c')), - (_n('c', 'C'), FAIL, _n('C', 'c')), - (_n('mag ical ', ' tre vor'), FAIL, _n(' tre vor', 'mag ical ')), - (_n('@', '-', '_', '#'), FAIL, _n('#', '-', '@', '_')), + (_n(b'alpha', b'beta'), PASS, _n(b'alpha', b'beta')), + (_n(b'beta', b'alpha'), FAIL, _n(b'alpha', b'beta')), + (_n(b'C', b'c'), PASS, _n(b'C', b'c')), + (_n(b'c', b'C'), FAIL, _n(b'C', b'c')), + (_n(b'mag ical ', b' tre vor'), FAIL, _n(b' tre vor', b'mag ical ')), + (_n(b'@', b'-', b'_', b'#'), FAIL, _n(b'#', b'-', b'@', b'_')), ) @@ -42,13 +39,14 @@ def test_parse_commandline_input_errors_without_args(): with pytest.raises(SystemExit): parse_commandline_input([]) + @pytest.mark.parametrize( - ('filename_list'), + ('filename_list'), ( - ['filename1'], + ['filename1'], ['filename1', 'filename2'], ) ) def test_parse_commandline_input_success(filename_list): args = parse_commandline_input(filename_list) - assert args.filenames == filename_list \ No newline at end of file + assert args.filenames == filename_list From b941d0e6dfb07cfd278fe958bcd278dc3936616b Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Fri, 23 Jun 2017 14:58:24 -0700 Subject: [PATCH 4/7] Respond to review feedback --- .pre-commit-hooks.yaml | 2 +- pre_commit_hooks/file_contents_sorter.py | 2 +- requirements-dev.txt | 1 - tests/file_contents_sorter_test.py | 18 +++++++----------- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index d501d50..eea7bed 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -110,7 +110,7 @@ description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. entry: file-contents-sorter language: python - files: '' + files: '^$' - id: fix-encoding-pragma name: Fix python encoding pragma language: python diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index 06c6d3a..e01eb8c 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -18,7 +18,7 @@ FAIL = 1 def sort_file_contents(f): - before = [line for line in f] + before = list(f) after = sorted(before) before_string = b''.join(before) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4070e66..2922ef5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,6 @@ coverage flake8 -ipdb mock pre-commit pytest diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index 4e65629..e8f1ea8 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -6,21 +6,17 @@ from pre_commit_hooks.file_contents_sorter import parse_commandline_input from pre_commit_hooks.file_contents_sorter import PASS -def _n(*strs): - return b'\n'.join(strs) + b'\n' - - # Input, expected return value, expected output TESTS = ( (b'', PASS, b''), - (_n(b'lonesome'), PASS, _n(b'lonesome')), + (b'lonesome\n', PASS, b'lonesome\n'), (b'missing_newline', PASS, b'missing_newline'), - (_n(b'alpha', b'beta'), PASS, _n(b'alpha', b'beta')), - (_n(b'beta', b'alpha'), FAIL, _n(b'alpha', b'beta')), - (_n(b'C', b'c'), PASS, _n(b'C', b'c')), - (_n(b'c', b'C'), FAIL, _n(b'C', b'c')), - (_n(b'mag ical ', b' tre vor'), FAIL, _n(b' tre vor', b'mag ical ')), - (_n(b'@', b'-', b'_', b'#'), FAIL, _n(b'#', b'-', b'@', b'_')), + (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'), + (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'), + (b'C\nc\n', PASS, b'C\nc\n'), + (b'c\nC\n', FAIL, b'C\nc\n'), + (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'), + (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'), ) From 05d9c8c8051446dc14aec45a859f39ae22c01bdb Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Fri, 23 Jun 2017 15:10:10 -0700 Subject: [PATCH 5/7] Make tests pass --- hooks.yaml | 2 +- tests/file_contents_sorter_test.py | 18 ------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/hooks.yaml b/hooks.yaml index d501d50..eea7bed 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -110,7 +110,7 @@ description: Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input in your .pre-commit-config.yaml file. entry: file-contents-sorter language: python - files: '' + files: '^$' - id: fix-encoding-pragma name: Fix python encoding pragma language: python diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index e8f1ea8..7b3d098 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -2,7 +2,6 @@ import pytest from pre_commit_hooks.file_contents_sorter import FAIL from pre_commit_hooks.file_contents_sorter import main -from pre_commit_hooks.file_contents_sorter import parse_commandline_input from pre_commit_hooks.file_contents_sorter import PASS @@ -29,20 +28,3 @@ def test_integration(input_s, expected_retval, output, tmpdir): assert path.read_binary() == output assert output_retval == expected_retval - - -def test_parse_commandline_input_errors_without_args(): - with pytest.raises(SystemExit): - parse_commandline_input([]) - - -@pytest.mark.parametrize( - ('filename_list'), - ( - ['filename1'], - ['filename1', 'filename2'], - ) -) -def test_parse_commandline_input_success(filename_list): - args = parse_commandline_input(filename_list) - assert args.filenames == filename_list From 89ddf178883e96a1f4452cc30df287deb0f624c4 Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Sun, 25 Jun 2017 09:48:16 -0700 Subject: [PATCH 6/7] Inline tuple parameterized test tuple --- tests/file_contents_sorter_test.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py index 7b3d098..5f4dc5b 100644 --- a/tests/file_contents_sorter_test.py +++ b/tests/file_contents_sorter_test.py @@ -5,21 +5,20 @@ from pre_commit_hooks.file_contents_sorter import main from pre_commit_hooks.file_contents_sorter import PASS -# Input, expected return value, expected output -TESTS = ( - (b'', PASS, b''), - (b'lonesome\n', PASS, b'lonesome\n'), - (b'missing_newline', PASS, b'missing_newline'), - (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'), - (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'), - (b'C\nc\n', PASS, b'C\nc\n'), - (b'c\nC\n', FAIL, b'C\nc\n'), - (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'), - (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'), +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + ( + (b'', PASS, b''), + (b'lonesome\n', PASS, b'lonesome\n'), + (b'missing_newline', PASS, b'missing_newline'), + (b'alpha\nbeta\n', PASS, b'alpha\nbeta\n'), + (b'beta\nalpha\n', FAIL, b'alpha\nbeta\n'), + (b'C\nc\n', PASS, b'C\nc\n'), + (b'c\nC\n', FAIL, b'C\nc\n'), + (b'mag ical \n tre vor\n', FAIL, b' tre vor\nmag ical \n'), + (b'@\n-\n_\n#\n', FAIL, b'#\n-\n@\n_\n'), + ) ) - - -@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS) def test_integration(input_s, expected_retval, output, tmpdir): path = tmpdir.join('file.txt') path.write_binary(input_s) From 4c421e2ed1da0ed6638003c1c94c3bb13894d94e Mon Sep 17 00:00:00 2001 From: Daniel Gallagher Date: Sun, 25 Jun 2017 10:22:10 -0700 Subject: [PATCH 7/7] Put argument parsing back into main() --- pre_commit_hooks/file_contents_sorter.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pre_commit_hooks/file_contents_sorter.py b/pre_commit_hooks/file_contents_sorter.py index e01eb8c..6fa3bc0 100644 --- a/pre_commit_hooks/file_contents_sorter.py +++ b/pre_commit_hooks/file_contents_sorter.py @@ -18,7 +18,7 @@ FAIL = 1 def sort_file_contents(f): - before = list(f) + before = tuple(f) after = sorted(before) before_string = b''.join(before) @@ -33,15 +33,10 @@ def sort_file_contents(f): return FAIL -def parse_commandline_input(argv): +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument('filenames', nargs='+', help='Files to sort') args = parser.parse_args(argv) - return args - - -def main(argv=None): - args = parse_commandline_input(argv) retv = PASS