From 3b8b26a0971a09628c125f166855dfbc1d2fd891 Mon Sep 17 00:00:00 2001 From: "zhiwei.meng" Date: Wed, 1 Apr 2026 17:16:02 +0800 Subject: [PATCH] Add "--check" option support to end_of_file_fixer hook --- pre_commit_hooks/end_of_file_fixer.py | 20 ++++++---- tests/end_of_file_fixer_test.py | 56 +++++++++++++++++++-------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/pre_commit_hooks/end_of_file_fixer.py b/pre_commit_hooks/end_of_file_fixer.py index a88425c..bb00fec 100644 --- a/pre_commit_hooks/end_of_file_fixer.py +++ b/pre_commit_hooks/end_of_file_fixer.py @@ -6,7 +6,7 @@ from collections.abc import Sequence from typing import IO -def fix_file(file_obj: IO[bytes]) -> int: +def fix_file(file_obj: IO[bytes], check_only=False) -> int: # Test for newline at end of file # Empty files will throw IOError here try: @@ -18,7 +18,8 @@ def fix_file(file_obj: IO[bytes]) -> int: if last_character not in {b'\n', b'\r'} and last_character != b'': # Needs this seek for windows, otherwise IOError file_obj.seek(0, os.SEEK_END) - file_obj.write(b'\n') + if not check_only: + file_obj.write(b'\n') return 1 while last_character in {b'\n', b'\r'}: @@ -27,7 +28,8 @@ def fix_file(file_obj: IO[bytes]) -> int: # 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() + if not check_only: + file_obj.truncate() return 1 # Go back two bytes and read a character @@ -43,7 +45,8 @@ def fix_file(file_obj: IO[bytes]) -> int: return 0 elif remaining.startswith(sequence): file_obj.seek(position + len(sequence)) - file_obj.truncate() + if not check_only: + file_obj.truncate() return 1 return 0 @@ -51,6 +54,7 @@ def fix_file(file_obj: IO[bytes]) -> int: def main(argv: Sequence[str] | None = None) -> int: parser = argparse.ArgumentParser() + parser.add_argument('--check', action='store_true', help='Check without fixing') parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) @@ -59,11 +63,13 @@ def main(argv: Sequence[str] | None = None) -> int: 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) + ret_for_file = fix_file(file_obj, args.check) if ret_for_file: - print(f'Fixing {filename}') + if args.check: + print(f'Wrong ending of file: {filename}') + else: + print(f'Fixing {filename}') retv |= ret_for_file - return retv diff --git a/tests/end_of_file_fixer_test.py b/tests/end_of_file_fixer_test.py index 8a5d889..a5349d5 100644 --- a/tests/end_of_file_fixer_test.py +++ b/tests/end_of_file_fixer_test.py @@ -10,34 +10,56 @@ from pre_commit_hooks.end_of_file_fixer import main # Input, expected return value, expected output TESTS = ( - (b'foo\n', 0, b'foo\n'), - (b'', 0, b''), - (b'\n\n', 1, b''), - (b'\n\n\n\n', 1, b''), - (b'foo', 1, b'foo\n'), - (b'foo\n\n\n', 1, b'foo\n'), - (b'\xe2\x98\x83', 1, b'\xe2\x98\x83\n'), - (b'foo\r\n', 0, b'foo\r\n'), - (b'foo\r\n\r\n\r\n', 1, b'foo\r\n'), - (b'foo\r', 0, b'foo\r'), - (b'foo\r\r\r\r', 1, b'foo\r'), + (b'foo\n', 0, b'foo\n', None), + (b'', 0, b'', None), + (b'\n\n', 1, b'', None), + (b'\n\n\n\n', 1, b'', None), + (b'foo', 1, b'foo\n', None), + (b'foo\n\n\n', 1, b'foo\n', None), + (b'\xe2\x98\x83', 1, b'\xe2\x98\x83\n', None), + (b'foo\r\n', 0, b'foo\r\n', None), + (b'foo\r\n\r\n\r\n', 1, b'foo\r\n', None), + (b'foo\r', 0, b'foo\r', None), + (b'foo\r\r\r\r', 1, b'foo\r', None), + + (b'foo\n', 0, b'foo\n', '--check'), + (b'', 0, b'', '--check'), + (b'\n\n', 1, b'\n\n', '--check'), + (b'\n\n\n\n', 1, b'\n\n\n\n', '--check'), + (b'foo', 1, b'foo', '--check'), + (b'foo\n\n\n', 1, b'foo\n\n\n', '--check'), + (b'\xe2\x98\x83', 1, b'\xe2\x98\x83', '--check'), + (b'foo\r\n', 0, b'foo\r\n', '--check'), + (b'foo\r\n\r\n\r\n', 1, b'foo\r\n\r\n\r\n', '--check'), + (b'foo\r', 0, b'foo\r', '--check'), + (b'foo\r\r\r\r', 1, b'foo\r\r\r\r', '--check'), ) -@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS) -def test_fix_file(input_s, expected_retval, output): +@pytest.mark.parametrize(('input_s', 'expected_retval', 'output', 'options'), TESTS) +def test_fix_file(input_s, expected_retval, output, options): + if options is None: + options = [] + elif isinstance(options, str): + options = [options] + file_obj = io.BytesIO(input_s) - ret = fix_file(file_obj) + ret = fix_file(file_obj, "--check" in [*options]) assert file_obj.getvalue() == output assert ret == expected_retval -@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS) -def test_integration(input_s, expected_retval, output, tmpdir): +@pytest.mark.parametrize(('input_s', 'expected_retval', 'output', 'options'), TESTS) +def test_integration(input_s, expected_retval, output, options, tmpdir): path = tmpdir.join('file.txt') path.write_binary(input_s) - ret = main([str(path)]) + if options is None: + options = [] + elif isinstance(options, str): + options = [options] + + ret = main([*options, str(path)]) file_output = path.read_binary() assert file_output == output