From fc8a5b27e9ec9e3022587a57a5e55b91cf0ee83b Mon Sep 17 00:00:00 2001 From: Morgan Courbet Date: Tue, 13 Jun 2017 21:38:14 +0200 Subject: [PATCH 1/3] Add mixed-line-ending hook --- .pre-commit-hooks.yaml | 9 ++ README.md | 5 + hooks.yaml | 6 + pre_commit_hooks/mixed_line_ending.py | 212 ++++++++++++++++++++++++++ setup.py | 2 + tests/mixed_line_ending_test.py | 154 +++++++++++++++++++ 6 files changed, 388 insertions(+) create mode 100644 pre_commit_hooks/mixed_line_ending.py create mode 100644 tests/mixed_line_ending_test.py diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 69e316a..454f258 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -191,6 +191,15 @@ # for backward compatibility files: '' minimum_pre_commit_version: 0.15.0 +- id: mixed-line-ending + name: Mixed line ending + description: Replaces or checks mixed line ending + entry: mixed-line-ending + language: python + types: [text] + # for backward compatibility + files: '' + minimum_pre_commit_version: 0.15.0 - id: name-tests-test name: Tests should end in _test.py description: This verifies that test files are named correctly diff --git a/README.md b/README.md index 7b4c486..8efd74b 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ Add this to your `.pre-commit-config.yaml` - `file-contents-sorter` - Sort the lines in specified files (defaults to alphabetical). You must provide list of target files as input to it. Note that this hook WILL remove blank lines and does NOT respect any comments. - `flake8` - Run flake8 on your python files. - `forbid-new-submodules` - Prevent addition of new git submodules. +- `mixed-line-ending` - Replaces or checks mixed line ending. + - `--fix={auto,crlf,lf,no}` + - `auto` - Replaces automatically the most frequent line ending. This is the default argument. + - `crlf`, `lf` - Forces to replace line ending by respectively CRLF and LF. + - `no` - Checks if there is any mixed line ending without modifying any file. - `name-tests-test` - Assert that files in tests/ end in `_test.py`. - Use `args: ['--django']` to match `test*.py` instead. - `no-commit-to-branch` - Protect specific branches from direct checkins. diff --git a/hooks.yaml b/hooks.yaml index 5278bf5..59cc320 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -130,6 +130,12 @@ entry: upgrade-your-pre-commit-version files: '' minimum_pre_commit_version: 0.15.0 +- id: mixed-line-ending + language: system + name: upgrade-your-pre-commit-version + entry: upgrade-your-pre-commit-version + files: '' + minimum_pre_commit_version: 0.15.0 - id: name-tests-test language: system name: upgrade-your-pre-commit-version diff --git a/pre_commit_hooks/mixed_line_ending.py b/pre_commit_hooks/mixed_line_ending.py new file mode 100644 index 0000000..76512a7 --- /dev/null +++ b/pre_commit_hooks/mixed_line_ending.py @@ -0,0 +1,212 @@ +import argparse +import re +import sys + +from enum import Enum + + +class LineEnding(Enum): + CR = b'\r', 'cr', re.compile(b'\r(?!\n)', re.DOTALL) + CRLF = b'\r\n', 'crlf', re.compile(b'\r\n', re.DOTALL) + LF = b'\n', 'lf', re.compile(b'(? 0 + + mixed |= le_found_previously and le_found_cur + le_found_previously |= le_found_cur + + if le_count == max_le_count: + most_le = None + elif le_count > max_le_count: + max_le_count = le_count + most_le = le + + if not mixed: + return MixedLineDetection.NOT_MIXED + + for mld in MixedLineDetection: + if ( + mld.line_ending_enum is not None and + mld.line_ending_enum == most_le + ): + return mld + + return MixedLineDetection.UNKNOWN + + +def _process_no_fix(filenames): + print('Checking if the files have mixed line ending.') + + mle_filenames = [] + for filename in filenames: + detect_result = _detect_line_ending(filename) + + if detect_result.mle_found: + mle_filenames.append(filename) + + mle_found = len(mle_filenames) > 0 + + if mle_found: + print( + 'The following files have mixed line endings:\n\t%s', + '\n\t'.join(mle_filenames), + ) + + return 1 if mle_found else 0 + + +def _process_fix_auto(filenames): + mle_found = False + + for filename in filenames: + detect_result = _detect_line_ending(filename) + + if detect_result == MixedLineDetection.NOT_MIXED: + print('The file %s has no mixed line ending', filename) + elif detect_result == MixedLineDetection.UNKNOWN: + print( + 'Could not define most frequent line ending in ' + 'file %s. File skiped.', filename, + ) + + mle_found = True + else: + le_enum = detect_result.line_ending_enum + + print( + 'The file %s has mixed line ending with a ' + 'majority of %s. Converting...', filename, le_enum.str_print, + ) + + _convert_line_ending(filename, le_enum.string) + mle_found = True + + print( + 'The file %s has been converted to %s line ending.', + filename, le_enum.str_print, + ) + + return 1 if mle_found else 0 + + +def _process_fix_force(filenames, line_ending_enum): + for filename in filenames: + _convert_line_ending(filename, line_ending_enum.string) + + print( + 'The file %s has been forced to %s line ending.', + filename, line_ending_enum.str_print, + ) + + return 1 + + +def _convert_line_ending(filename, line_ending): + with open(filename, 'rb+') as f: + bufin = f.read() + + # convert line ending + bufout = ANY_LINE_ENDING_PATTERN.sub(line_ending, bufin) + + # write the result in the file replacing the existing content + f.seek(0) + f.write(bufout) + f.truncate() + + +if __name__ == '__main__': + sys.exit(mixed_line_ending()) diff --git a/setup.py b/setup.py index 4c8c148..432c19a 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ setup( 'simplejson', 'six', ], + extras_require={':python_version=="2.7"': ['enum34']}, entry_points={ 'console_scripts': [ 'autopep8-wrapper = pre_commit_hooks.autopep8_wrapper:main', @@ -53,6 +54,7 @@ setup( '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', + 'mixed-line-ending = pre_commit_hooks.mixed_line_ending:mixed_line_ending', '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', diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py new file mode 100644 index 0000000..9b49b5e --- /dev/null +++ b/tests/mixed_line_ending_test.py @@ -0,0 +1,154 @@ +import pytest + +from pre_commit_hooks.mixed_line_ending import mixed_line_ending + +# Input, expected return value, expected output +TESTS_FIX_AUTO = ( + # only 'LF' + (b'foo\nbar\nbaz\n', 0, b'foo\nbar\nbaz\n'), + # only 'CRLF' + (b'foo\r\nbar\r\nbaz\r\n', 0, b'foo\r\nbar\r\nbaz\r\n'), + # only 'CR' + (b'foo\rbar\rbaz\r', 0, b'foo\rbar\rbaz\r'), + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', 1, b'foo\rbar\rbaz\r'), + # mixed with as much 'LF' as 'CRLF' + (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\nbaz'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\nbaz', 1, b'foo\rbar\nbaz'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\nbaz'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', 1, b'foo\r\nbar\nbaz\r'), +) + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + TESTS_FIX_AUTO, +) +def test_mixed_line_ending_fix_auto(input_s, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write(input_s) + ret = mixed_line_ending(('--fix=auto', path.strpath)) + + assert ret == expected_retval + assert path.read_binary() == output + + +# Input, expected return value, expected output +TESTS_NO_FIX = ( + # only 'LF' + (b'foo\nbar\nbaz\n', 0, b'foo\nbar\nbaz\n'), + # only 'CRLF' + (b'foo\r\nbar\r\nbaz\r\n', 0, b'foo\r\nbar\r\nbaz\r\n'), + # only 'CR' + (b'foo\rbar\rbaz\r', 0, b'foo\rbar\rbaz\r'), + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', 1, b'foo\r\nbar\nbaz\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\nbaz\r\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', 1, b'foo\rbar\nbaz\r'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\nbaz', 0, b'foo\rbar\nbaz'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\nbaz', 0, b'foo\r\nbar\nbaz'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', 0, b'foo\r\nbar\nbaz\r'), +) + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + TESTS_NO_FIX, +) +def test_detect_mixed_line_ending(input_s, expected_retval, output, tmpdir): + path = tmpdir.join('file.txt') + path.write(input_s) + ret = mixed_line_ending(('--fix=no', path.strpath)) + + assert ret == expected_retval + assert path.read_binary() == output + + +# Input, expected return value, expected output +TESTS_FIX_FORCE_LF = ( + # only 'LF' + (b'foo\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), + # only 'CRLF' + (b'foo\r\nbar\r\nbaz\r\n', 1, b'foo\nbar\nbaz\n'), + # only 'CR' + (b'foo\rbar\rbaz\r', 1, b'foo\nbar\nbaz\n'), + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', 1, b'foo\nbar\nbaz\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', 1, b'foo\nbar\nbaz\n'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\nbaz', 1, b'foo\nbar\nbaz'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\nbaz', 1, b'foo\nbar\nbaz'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', 1, b'foo\nbar\nbaz\n'), +) + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + TESTS_FIX_FORCE_LF, +) +def test_mixed_line_ending_fix_force_lf( + input_s, expected_retval, output, + tmpdir, +): + path = tmpdir.join('file.txt') + path.write(input_s) + ret = mixed_line_ending(('--fix=lf', path.strpath)) + + assert ret == expected_retval + assert path.read_binary() == output + + +# Input, expected return value, expected output +TESTS_FIX_FORCE_CRLF = ( + # only 'LF' + (b'foo\nbar\nbaz\n', 1, b'foo\r\nbar\r\nbaz\r\n'), + # only 'CRLF' + (b'foo\r\nbar\r\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), + # only 'CR' + (b'foo\rbar\rbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', 1, b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\nbaz', 1, b'foo\r\nbar\r\nbaz'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\r\nbaz'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), +) + + +@pytest.mark.parametrize( + ('input_s', 'expected_retval', 'output'), + TESTS_FIX_FORCE_CRLF, +) +def test_mixed_line_ending_fix_force_crlf( + input_s, expected_retval, output, + tmpdir, +): + path = tmpdir.join('file.txt') + path.write(input_s) + ret = mixed_line_ending(('--fix=crlf', path.strpath)) + + assert ret == expected_retval + assert path.read_binary() == output From 47c4d9ebedb4760b84c15f504da8b9b04a8833f5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 19:27:34 -0700 Subject: [PATCH 2/3] Fix mixed-line-ending tests on windows --- tests/mixed_line_ending_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py index 9b49b5e..20fd22c 100644 --- a/tests/mixed_line_ending_test.py +++ b/tests/mixed_line_ending_test.py @@ -33,7 +33,7 @@ TESTS_FIX_AUTO = ( ) def test_mixed_line_ending_fix_auto(input_s, expected_retval, output, tmpdir): path = tmpdir.join('file.txt') - path.write(input_s) + path.write_binary(input_s) ret = mixed_line_ending(('--fix=auto', path.strpath)) assert ret == expected_retval @@ -69,7 +69,7 @@ TESTS_NO_FIX = ( ) def test_detect_mixed_line_ending(input_s, expected_retval, output, tmpdir): path = tmpdir.join('file.txt') - path.write(input_s) + path.write_binary(input_s) ret = mixed_line_ending(('--fix=no', path.strpath)) assert ret == expected_retval @@ -108,7 +108,7 @@ def test_mixed_line_ending_fix_force_lf( tmpdir, ): path = tmpdir.join('file.txt') - path.write(input_s) + path.write_binary(input_s) ret = mixed_line_ending(('--fix=lf', path.strpath)) assert ret == expected_retval @@ -147,7 +147,7 @@ def test_mixed_line_ending_fix_force_crlf( tmpdir, ): path = tmpdir.join('file.txt') - path.write(input_s) + path.write_binary(input_s) ret = mixed_line_ending(('--fix=crlf', path.strpath)) assert ret == expected_retval From fbcd096ea91195af3b47a99a9c7799203231db3d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 5 Sep 2017 20:20:43 -0700 Subject: [PATCH 3/3] Simplify mixed-line-ending hook --- pre_commit_hooks/mixed_line_ending.py | 243 ++++++-------------------- setup.py | 1 - tests/mixed_line_ending_test.py | 205 ++++++++-------------- 3 files changed, 134 insertions(+), 315 deletions(-) diff --git a/pre_commit_hooks/mixed_line_ending.py b/pre_commit_hooks/mixed_line_ending.py index 76512a7..301c654 100644 --- a/pre_commit_hooks/mixed_line_ending.py +++ b/pre_commit_hooks/mixed_line_ending.py @@ -1,212 +1,83 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + import argparse -import re -import sys - -from enum import Enum +import collections -class LineEnding(Enum): - CR = b'\r', 'cr', re.compile(b'\r(?!\n)', re.DOTALL) - CRLF = b'\r\n', 'crlf', re.compile(b'\r\n', re.DOTALL) - LF = b'\n', 'lf', re.compile(b'(? 1 + if fix == 'no' or (fix == 'auto' and not mixed): + return mixed -def mixed_line_ending(argv=None): - options = _parse_arguments(argv) + if fix == 'auto': + max_ending = LF + max_lines = 0 + # ordering is important here such that lf > crlf > cr + for ending_type in ALL_ENDINGS: + # also important, using >= to find a max that prefers the last + if counts[ending_type] >= max_lines: + max_ending = ending_type + max_lines = counts[ending_type] - filenames = options['filenames'] - fix_option = options['fix'] - - if fix_option == MixedLineEndingOption.NO: - return _process_no_fix(filenames) - elif fix_option == MixedLineEndingOption.AUTO: - return _process_fix_auto(filenames) - # when a line ending character is forced with --fix option + _fix(filename, contents, max_ending) + return 1 else: - return _process_fix_force(filenames, fix_option.line_ending_enum) + target_ending = FIX_TO_LINE_ENDING[fix] + # find if there are lines with *other* endings + del counts[target_ending] + other_endings = bool(sum(counts.values())) + if other_endings: + _fix(filename, contents, target_ending) + return other_endings -def _parse_arguments(argv=None): +def main(argv=None): parser = argparse.ArgumentParser() parser.add_argument( - '-f', - '--fix', - choices=[m.opt_name for m in MixedLineEndingOption], - default=MixedLineEndingOption.AUTO.opt_name, + '-f', '--fix', + choices=('auto', 'no') + tuple(FIX_TO_LINE_ENDING), + default='auto', help='Replace line ending with the specified. Default is "auto"', ) parser.add_argument('filenames', nargs='*', help='Filenames to fix') args = parser.parse_args(argv) - fix, = ( - member for name, member - in MixedLineEndingOption.__members__.items() - if member.opt_name == args.fix - ) - - options = { - 'fix': fix, 'filenames': args.filenames, - } - - return options - - -def _detect_line_ending(filename): - with open(filename, 'rb') as f: - buf = f.read() - - le_counts = {} - - for le_enum in LineEnding: - le_counts[le_enum] = len(le_enum.regex.findall(buf)) - - mixed = False - le_found_previously = False - most_le = None - max_le_count = 0 - - for le, le_count in le_counts.items(): - le_found_cur = le_count > 0 - - mixed |= le_found_previously and le_found_cur - le_found_previously |= le_found_cur - - if le_count == max_le_count: - most_le = None - elif le_count > max_le_count: - max_le_count = le_count - most_le = le - - if not mixed: - return MixedLineDetection.NOT_MIXED - - for mld in MixedLineDetection: - if ( - mld.line_ending_enum is not None and - mld.line_ending_enum == most_le - ): - return mld - - return MixedLineDetection.UNKNOWN - - -def _process_no_fix(filenames): - print('Checking if the files have mixed line ending.') - - mle_filenames = [] - for filename in filenames: - detect_result = _detect_line_ending(filename) - - if detect_result.mle_found: - mle_filenames.append(filename) - - mle_found = len(mle_filenames) > 0 - - if mle_found: - print( - 'The following files have mixed line endings:\n\t%s', - '\n\t'.join(mle_filenames), - ) - - return 1 if mle_found else 0 - - -def _process_fix_auto(filenames): - mle_found = False - - for filename in filenames: - detect_result = _detect_line_ending(filename) - - if detect_result == MixedLineDetection.NOT_MIXED: - print('The file %s has no mixed line ending', filename) - elif detect_result == MixedLineDetection.UNKNOWN: - print( - 'Could not define most frequent line ending in ' - 'file %s. File skiped.', filename, - ) - - mle_found = True - else: - le_enum = detect_result.line_ending_enum - - print( - 'The file %s has mixed line ending with a ' - 'majority of %s. Converting...', filename, le_enum.str_print, - ) - - _convert_line_ending(filename, le_enum.string) - mle_found = True - - print( - 'The file %s has been converted to %s line ending.', - filename, le_enum.str_print, - ) - - return 1 if mle_found else 0 - - -def _process_fix_force(filenames, line_ending_enum): - for filename in filenames: - _convert_line_ending(filename, line_ending_enum.string) - - print( - 'The file %s has been forced to %s line ending.', - filename, line_ending_enum.str_print, - ) - - return 1 - - -def _convert_line_ending(filename, line_ending): - with open(filename, 'rb+') as f: - bufin = f.read() - - # convert line ending - bufout = ANY_LINE_ENDING_PATTERN.sub(line_ending, bufin) - - # write the result in the file replacing the existing content - f.seek(0) - f.write(bufout) - f.truncate() + retv = 0 + for filename in args.filenames: + retv |= fix_filename(filename, args.fix) + return retv if __name__ == '__main__': - sys.exit(mixed_line_ending()) + exit(main()) diff --git a/setup.py b/setup.py index 432c19a..2563095 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,6 @@ setup( 'simplejson', 'six', ], - extras_require={':python_version=="2.7"': ['enum34']}, entry_points={ 'console_scripts': [ 'autopep8-wrapper = pre_commit_hooks.autopep8_wrapper:main', diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py index 20fd22c..808295b 100644 --- a/tests/mixed_line_ending_test.py +++ b/tests/mixed_line_ending_test.py @@ -1,154 +1,103 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + import pytest -from pre_commit_hooks.mixed_line_ending import mixed_line_ending - -# Input, expected return value, expected output -TESTS_FIX_AUTO = ( - # only 'LF' - (b'foo\nbar\nbaz\n', 0, b'foo\nbar\nbaz\n'), - # only 'CRLF' - (b'foo\r\nbar\r\nbaz\r\n', 0, b'foo\r\nbar\r\nbaz\r\n'), - # only 'CR' - (b'foo\rbar\rbaz\r', 0, b'foo\rbar\rbaz\r'), - # mixed with majority of 'LF' - (b'foo\r\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), - # mixed with majority of 'CRLF' - (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), - # mixed with majority of 'CR' - (b'foo\rbar\nbaz\r', 1, b'foo\rbar\rbaz\r'), - # mixed with as much 'LF' as 'CRLF' - (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\nbaz'), - # mixed with as much 'LF' as 'CR' - (b'foo\rbar\nbaz', 1, b'foo\rbar\nbaz'), - # mixed with as much 'CRLF' as 'CR' - (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\nbaz'), - # mixed with as much 'CRLF' as 'LF' as 'CR' - (b'foo\r\nbar\nbaz\r', 1, b'foo\r\nbar\nbaz\r'), -) +from pre_commit_hooks.mixed_line_ending import main @pytest.mark.parametrize( - ('input_s', 'expected_retval', 'output'), - TESTS_FIX_AUTO, + ('input_s', 'output'), + ( + # mixed with majority of 'LF' + (b'foo\r\nbar\nbaz\n', b'foo\nbar\nbaz\n'), + # mixed with majority of 'CRLF' + (b'foo\r\nbar\nbaz\r\n', b'foo\r\nbar\r\nbaz\r\n'), + # mixed with majority of 'CR' + (b'foo\rbar\nbaz\r', b'foo\rbar\rbaz\r'), + # mixed with as much 'LF' as 'CRLF' + (b'foo\r\nbar\n', b'foo\nbar\n'), + # mixed with as much 'LF' as 'CR' + (b'foo\rbar\n', b'foo\nbar\n'), + # mixed with as much 'CRLF' as 'CR' + (b'foo\r\nbar\r', b'foo\r\nbar\r\n'), + # mixed with as much 'CRLF' as 'LF' as 'CR' + (b'foo\r\nbar\nbaz\r', b'foo\nbar\nbaz\n'), + ), ) -def test_mixed_line_ending_fix_auto(input_s, expected_retval, output, tmpdir): +def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir): path = tmpdir.join('file.txt') path.write_binary(input_s) - ret = mixed_line_ending(('--fix=auto', path.strpath)) + ret = main((path.strpath,)) - assert ret == expected_retval + assert ret == 1 assert path.read_binary() == output -# Input, expected return value, expected output -TESTS_NO_FIX = ( - # only 'LF' - (b'foo\nbar\nbaz\n', 0, b'foo\nbar\nbaz\n'), - # only 'CRLF' - (b'foo\r\nbar\r\nbaz\r\n', 0, b'foo\r\nbar\r\nbaz\r\n'), - # only 'CR' - (b'foo\rbar\rbaz\r', 0, b'foo\rbar\rbaz\r'), - # mixed with majority of 'LF' - (b'foo\r\nbar\nbaz\n', 1, b'foo\r\nbar\nbaz\n'), - # mixed with majority of 'CRLF' - (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\nbaz\r\n'), - # mixed with majority of 'CR' - (b'foo\rbar\nbaz\r', 1, b'foo\rbar\nbaz\r'), - # mixed with as much 'LF' as 'CR' - (b'foo\rbar\nbaz', 0, b'foo\rbar\nbaz'), - # mixed with as much 'CRLF' as 'CR' - (b'foo\r\nbar\nbaz', 0, b'foo\r\nbar\nbaz'), - # mixed with as much 'CRLF' as 'LF' as 'CR' - (b'foo\r\nbar\nbaz\r', 0, b'foo\r\nbar\nbaz\r'), -) +def test_non_mixed_no_newline_end_of_file(tmpdir): + path = tmpdir.join('f.txt') + path.write_binary(b'foo\nbar\nbaz') + assert not main((path.strpath,)) + # the hook *could* fix the end of the file, but leaves it alone + # this is mostly to document the current behaviour + assert path.read_binary() == b'foo\nbar\nbaz' + + +def test_mixed_no_newline_end_of_file(tmpdir): + path = tmpdir.join('f.txt') + path.write_binary(b'foo\r\nbar\nbaz') + assert main((path.strpath,)) + # the hook rewrites the end of the file, this is slightly inconsistent + # with the non-mixed case but I think this is the better behaviour + # this is mostly to document the current behaviour + assert path.read_binary() == b'foo\nbar\nbaz\n' @pytest.mark.parametrize( - ('input_s', 'expected_retval', 'output'), - TESTS_NO_FIX, + ('fix_option', 'input_s'), + ( + # All --fix=auto with uniform line endings should be ok + ('--fix=auto', b'foo\r\nbar\r\nbaz\r\n'), + ('--fix=auto', b'foo\rbar\rbaz\r'), + ('--fix=auto', b'foo\nbar\nbaz\n'), + # --fix=crlf with crlf endings + ('--fix=crlf', b'foo\r\nbar\r\nbaz\r\n'), + # --fix=lf with lf endings + ('--fix=lf', b'foo\nbar\nbaz\n'), + ), ) -def test_detect_mixed_line_ending(input_s, expected_retval, output, tmpdir): - path = tmpdir.join('file.txt') +def test_line_endings_ok(fix_option, input_s, tmpdir): + path = tmpdir.join('input.txt') path.write_binary(input_s) - ret = mixed_line_ending(('--fix=no', path.strpath)) + ret = main((fix_option, path.strpath)) - assert ret == expected_retval - assert path.read_binary() == output + assert ret == 0 + assert path.read_binary() == input_s -# Input, expected return value, expected output -TESTS_FIX_FORCE_LF = ( - # only 'LF' - (b'foo\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), - # only 'CRLF' - (b'foo\r\nbar\r\nbaz\r\n', 1, b'foo\nbar\nbaz\n'), - # only 'CR' - (b'foo\rbar\rbaz\r', 1, b'foo\nbar\nbaz\n'), - # mixed with majority of 'LF' - (b'foo\r\nbar\nbaz\n', 1, b'foo\nbar\nbaz\n'), - # mixed with majority of 'CRLF' - (b'foo\r\nbar\nbaz\r\n', 1, b'foo\nbar\nbaz\n'), - # mixed with majority of 'CR' - (b'foo\rbar\nbaz\r', 1, b'foo\nbar\nbaz\n'), - # mixed with as much 'LF' as 'CR' - (b'foo\rbar\nbaz', 1, b'foo\nbar\nbaz'), - # mixed with as much 'CRLF' as 'CR' - (b'foo\r\nbar\nbaz', 1, b'foo\nbar\nbaz'), - # mixed with as much 'CRLF' as 'LF' as 'CR' - (b'foo\r\nbar\nbaz\r', 1, b'foo\nbar\nbaz\n'), -) +def test_no_fix_does_not_modify(tmpdir): + path = tmpdir.join('input.txt') + contents = b'foo\r\nbar\rbaz\nwomp\n' + path.write_binary(contents) + ret = main(('--fix=no', path.strpath)) + + assert ret == 1 + assert path.read_binary() == contents -@pytest.mark.parametrize( - ('input_s', 'expected_retval', 'output'), - TESTS_FIX_FORCE_LF, -) -def test_mixed_line_ending_fix_force_lf( - input_s, expected_retval, output, - tmpdir, -): - path = tmpdir.join('file.txt') - path.write_binary(input_s) - ret = mixed_line_ending(('--fix=lf', path.strpath)) +def test_fix_lf(tmpdir): + path = tmpdir.join('input.txt') + path.write_binary(b'foo\r\nbar\rbaz\n') + ret = main(('--fix=lf', path.strpath)) - assert ret == expected_retval - assert path.read_binary() == output + assert ret == 1 + assert path.read_binary() == b'foo\nbar\nbaz\n' -# Input, expected return value, expected output -TESTS_FIX_FORCE_CRLF = ( - # only 'LF' - (b'foo\nbar\nbaz\n', 1, b'foo\r\nbar\r\nbaz\r\n'), - # only 'CRLF' - (b'foo\r\nbar\r\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), - # only 'CR' - (b'foo\rbar\rbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), - # mixed with majority of 'LF' - (b'foo\r\nbar\nbaz\n', 1, b'foo\r\nbar\r\nbaz\r\n'), - # mixed with majority of 'CRLF' - (b'foo\r\nbar\nbaz\r\n', 1, b'foo\r\nbar\r\nbaz\r\n'), - # mixed with majority of 'CR' - (b'foo\rbar\nbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), - # mixed with as much 'LF' as 'CR' - (b'foo\rbar\nbaz', 1, b'foo\r\nbar\r\nbaz'), - # mixed with as much 'CRLF' as 'CR' - (b'foo\r\nbar\nbaz', 1, b'foo\r\nbar\r\nbaz'), - # mixed with as much 'CRLF' as 'LF' as 'CR' - (b'foo\r\nbar\nbaz\r', 1, b'foo\r\nbar\r\nbaz\r\n'), -) +def test_fix_crlf(tmpdir): + path = tmpdir.join('input.txt') + path.write_binary(b'foo\r\nbar\rbaz\n') + ret = main(('--fix=crlf', path.strpath)) - -@pytest.mark.parametrize( - ('input_s', 'expected_retval', 'output'), - TESTS_FIX_FORCE_CRLF, -) -def test_mixed_line_ending_fix_force_crlf( - input_s, expected_retval, output, - tmpdir, -): - path = tmpdir.join('file.txt') - path.write_binary(input_s) - ret = mixed_line_ending(('--fix=crlf', path.strpath)) - - assert ret == expected_retval - assert path.read_binary() == output + assert ret == 1 + assert path.read_binary() == b'foo\r\nbar\r\nbaz\r\n'