Merge pull request #1 from pre-commit/end_of_file_fixer

Added end of file fixer hook.
This commit is contained in:
Anthony Sottile 2014-03-22 21:43:36 -07:00
commit 7673990291
4 changed files with 118 additions and 0 deletions

View File

@ -19,3 +19,8 @@
description: This verifies that test files are named correctly
entry: name-tests-test
language: python
- id: end-of-file-fixer
name: Fix End of Files
description: Ensures that a file is either empty, or ends with one newline.
entry: end-of-file-fixer
language: python

View File

@ -0,0 +1,70 @@
from __future__ import print_function
from __future__ import unicode_literals
import argparse
import os
import sys
from pre_commit_hooks.util import entry
def fix_file(file_obj):
# Test for newline at end of file
# Empty files will throw IOError here
try:
file_obj.seek(-1, os.SEEK_END)
except IOError:
return 0
last_character = file_obj.read(1)
# last_character will be '' for an empty file
if last_character != '\n' and last_character != '':
file_obj.write('\n')
return 1
while last_character == '\n':
# Deal with the beginning of the file
if file_obj.tell() == 1:
# If we've reached the beginning of the file and it is all
# linebreaks then we can make this file empty
file_obj.seek(0)
file_obj.truncate()
return 1
# Go back two bytes and read a character
file_obj.seek(-2, os.SEEK_CUR)
last_character = file_obj.read(1)
# Our current position is at the end of the file just before any amount of
# newlines. If we read two characters and get two newlines back we know
# there are extraneous newlines at the ned of the file. Then backtrack and
# trim the end off.
if len(file_obj.read(2)) == 2:
file_obj.seek(-1, os.SEEK_CUR)
file_obj.truncate()
return 1
return 0
@entry
def end_of_file_fixer(argv):
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to fix')
args = parser.parse_args(argv)
retv = 0
for filename in args.filenames:
# Read as binary so we can read byte-by-byte
with open(filename, 'rb+') as file_obj:
ret_for_file = fix_file(file_obj)
if ret_for_file:
print('Fixing {0}'.format(filename))
retv |= ret_for_file
return retv
if __name__ == '__main__':
sys.exit(end_of_file_fixer())

View File

@ -16,6 +16,7 @@ setup(
'debug-statement-hook = pre_commit_hooks.debug_statement_hook:debug_statement_hook',
'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:fix_trailing_whitespace',
'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files',
'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer',
],
},
)

View File

@ -0,0 +1,42 @@
import cStringIO
import os.path
import pytest
from pre_commit_hooks.end_of_file_fixer import end_of_file_fixer
from pre_commit_hooks.end_of_file_fixer import fix_file
# Input, expected return value, expected output
TESTS = (
('foo\n', 0, 'foo\n'),
('', 0, ''),
('\n\n', 1, ''),
('\n\n\n\n', 1, ''),
('foo', 1, 'foo\n'),
('foo\n\n\n', 1, 'foo\n'),
('\xe2\x98\x83', 1, '\xe2\x98\x83\n'),
)
@pytest.mark.parametrize(('input', 'expected_retval', 'output'), TESTS)
def test_fix_file(input, expected_retval, output):
file_obj = cStringIO.StringIO()
file_obj.write(input)
ret = fix_file(file_obj)
assert file_obj.getvalue() == output
assert ret == expected_retval
@pytest.mark.parametrize(('input', 'expected_retval', 'output'), TESTS)
def test_integration(input, expected_retval, output, tmpdir):
file_path = os.path.join(tmpdir.strpath, 'file.txt')
with open(file_path, 'w') as file_obj:
file_obj.write(input)
ret = end_of_file_fixer([file_path])
file_output = open(file_path, 'r').read()
assert file_output == output
assert ret == expected_retval