Skip to content

Commit 9425c5d

Browse files
author
Daniel Gallagher
committed
First commit of file-contents-sorter precommit hook
1 parent 78818b9 commit 9425c5d

File tree

5 files changed

+119
-0
lines changed

5 files changed

+119
-0
lines changed

.pre-commit-hooks.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@
105105
entry: end-of-file-fixer
106106
language: python
107107
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)$
108+
- id: file-contents-sorter
109+
name: File Contents Sorter
110+
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.
111+
entry: file-contents-sorter
112+
language: python
113+
files: ''
108114
- id: fix-encoding-pragma
109115
name: Fix python encoding pragma
110116
language: python
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
A very simple pre-commit hook that, when passed one or more filenames
3+
as arguments, will sort the lines in those files.
4+
5+
An example use case for this: you have a deploy-whitelist.txt file
6+
in a repo that contains a list of filenames that is used to specify
7+
files to be included in a docker container. This file has one filename
8+
per line. Various users are adding/removing lines from this file; using
9+
this hook on that file should reduce the instances of git merge
10+
conflicts and keep the file nicely ordered.
11+
"""
12+
from __future__ import print_function
13+
14+
import argparse
15+
16+
PASS = 0
17+
FAIL = 1
18+
19+
20+
def sort_file_contents(f):
21+
before = [line for line in f]
22+
after = sorted(before)
23+
24+
before_string = b''.join(before)
25+
after_string = b''.join(after)
26+
27+
if before_string == after_string:
28+
return PASS
29+
else:
30+
f.seek(0)
31+
f.write(after_string)
32+
f.truncate()
33+
return FAIL
34+
35+
36+
def parse_commandline_input(argv):
37+
parser = argparse.ArgumentParser()
38+
parser.add_argument('filenames', nargs='+', help='Files to sort')
39+
args = parser.parse_args(argv)
40+
return args
41+
42+
43+
def main(argv=None):
44+
args = parse_commandline_input(argv)
45+
46+
retv = PASS
47+
48+
for arg in args.filenames:
49+
with open(arg, 'rb+') as file_obj:
50+
ret_for_file = sort_file_contents(file_obj)
51+
52+
if ret_for_file:
53+
print('Sorting {}'.format(arg))
54+
55+
retv |= ret_for_file
56+
57+
return retv

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
coverage
44
flake8
5+
ipdb
56
mock
67
pre-commit
78
pytest

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
'detect-private-key = pre_commit_hooks.detect_private_key:detect_private_key',
5050
'double-quote-string-fixer = pre_commit_hooks.string_fixer:main',
5151
'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer',
52+
'file-contents-sorter = pre_commit_hooks.file_contents_sorter:main',
5253
'fix-encoding-pragma = pre_commit_hooks.fix_encoding_pragma:main',
5354
'forbid-new-submodules = pre_commit_hooks.forbid_new_submodules:main',
5455
'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files',

tests/file_contents_sorter_test.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from argparse import ArgumentError
2+
3+
import pytest
4+
5+
from pre_commit_hooks.file_contents_sorter import FAIL
6+
from pre_commit_hooks.file_contents_sorter import main
7+
from pre_commit_hooks.file_contents_sorter import parse_commandline_input
8+
from pre_commit_hooks.file_contents_sorter import PASS
9+
from pre_commit_hooks.file_contents_sorter import sort_file_contents
10+
11+
12+
def _n(*strs):
13+
return b'\n'.join(strs) + '\n'
14+
15+
16+
# Input, expected return value, expected output
17+
TESTS = (
18+
(b'', PASS, b''),
19+
(_n('lonesome'), PASS, _n('lonesome')),
20+
(b'missing_newline', PASS, b'missing_newline'),
21+
(_n('alpha', 'beta'), PASS, _n('alpha', 'beta')),
22+
(_n('beta', 'alpha'), FAIL, _n('alpha', 'beta')),
23+
(_n('C', 'c'), PASS, _n('C', 'c')),
24+
(_n('c', 'C'), FAIL, _n('C', 'c')),
25+
(_n('mag ical ', ' tre vor'), FAIL, _n(' tre vor', 'mag ical ')),
26+
(_n('@', '-', '_', '#'), FAIL, _n('#', '-', '@', '_')),
27+
)
28+
29+
30+
@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS)
31+
def test_integration(input_s, expected_retval, output, tmpdir):
32+
path = tmpdir.join('file.txt')
33+
path.write_binary(input_s)
34+
35+
output_retval = main([path.strpath])
36+
37+
assert path.read_binary() == output
38+
assert output_retval == expected_retval
39+
40+
41+
def test_parse_commandline_input_errors_without_args():
42+
with pytest.raises(SystemExit):
43+
parse_commandline_input([])
44+
45+
@pytest.mark.parametrize(
46+
('filename_list'),
47+
(
48+
['filename1'],
49+
['filename1', 'filename2'],
50+
)
51+
)
52+
def test_parse_commandline_input_success(filename_list):
53+
args = parse_commandline_input(filename_list)
54+
assert args.filenames == filename_list

0 commit comments

Comments
 (0)