diff --git a/README.md b/README.md index dcc030e..3eb8997 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Add this to your `.pre-commit-config.yaml` - `name-tests-test` - Assert that files in tests/ end in _test.py - `pyflakes` - Run pyflakes on your python files - `requirements-txt-fixer` - Sorts entries in requirements.txt +- `double-quote-string-fixer` - This hook replaces double quoted strings with single quoted strings - `trailing-whitespace` - Trims trailing whitespace. ### As a standalone package diff --git a/hooks.yaml b/hooks.yaml index d575170..9e32ce7 100644 --- a/hooks.yaml +++ b/hooks.yaml @@ -79,6 +79,12 @@ entry: requirements-txt-fixer language: python files: requirements.*\.txt$ +- id: double-quote-string-fixer + name: Fix double quoted strings + description: This hook replaces double quoted strings with single quoted strings + entry: double-quote-string-fixer + language: python + files: \.py$ - id: trailing-whitespace name: Trim Trailing Whitespace description: This hook trims trailing whitespace. diff --git a/pre_commit_hooks/string_fixer.py b/pre_commit_hooks/string_fixer.py new file mode 100644 index 0000000..616c08c --- /dev/null +++ b/pre_commit_hooks/string_fixer.py @@ -0,0 +1,65 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import argparse +import re +import tokenize + + +double_quote_starts = tuple(s for s in tokenize.single_quoted if '"' in s) +compiled_tokenize_string = re.compile(tokenize.String) + + +def handle_match(m): + string = m.group(0) + + for double_quote_start in double_quote_starts: + if string.startswith(double_quote_start): + meat = string[len(double_quote_start):-1] + if '"' in meat or "'" in meat: + break + return ( + double_quote_start.replace('"', "'") + + string[len(double_quote_start):-1] + + "'" + ) + return string + + +def fix_strings(filename): + return_value = 0 + + lines = [] + + with open(filename, 'r') as read_handle: + for line in read_handle: + if '"""' in line: + # Docstrings are hard, fuck it + lines.append(line) + else: + result = re.sub(compiled_tokenize_string, handle_match, line) + lines.append(result) + return_value |= int(result != line) + + with open(filename, 'w') as write_handle: + for line in lines: + write_handle.write(line) + + return return_value + + +def main(argv=None): + parser = argparse.ArgumentParser() + parser.add_argument('filenames', nargs='*', help='Filenames to fix') + args = parser.parse_args(argv) + + retv = 0 + + for filename in args.filenames: + return_value = fix_strings(filename) + if return_value != 0: + print('Fixing strings in {0}'.format(filename)) + retv |= return_value + + return retv diff --git a/setup.py b/setup.py index 4ed93f8..1a9cf2b 100644 --- a/setup.py +++ b/setup.py @@ -46,6 +46,7 @@ setup( 'debug-statement-hook = pre_commit_hooks.debug_statement_hook:debug_statement_hook', 'end-of-file-fixer = pre_commit_hooks.end_of_file_fixer:end_of_file_fixer', 'name-tests-test = pre_commit_hooks.tests_should_end_in_test:validate_files', + 'double-quote-string-fixer = pre_commit_hooks.string_fixer:main', 'requirements-txt-fixer = pre_commit_hooks.requirements_txt_fixer:fix_requirements_txt', 'trailing-whitespace-fixer = pre_commit_hooks.trailing_whitespace_fixer:fix_trailing_whitespace', ], diff --git a/tests/check_docstring_first_test.py b/tests/check_docstring_first_test.py index ecff0e3..f57e695 100644 --- a/tests/check_docstring_first_test.py +++ b/tests/check_docstring_first_test.py @@ -15,7 +15,7 @@ TESTS = ( ('', 0, ''), # Acceptable ('"foo"', 0, ''), - # Docstrin after code + # Docstring after code ( 'from __future__ import unicode_literals\n' '"foo"\n', diff --git a/tests/string_fixer_test.py b/tests/string_fixer_test.py new file mode 100644 index 0000000..15b9f19 --- /dev/null +++ b/tests/string_fixer_test.py @@ -0,0 +1,80 @@ +from __future__ import absolute_import +from __future__ import print_function +from __future__ import unicode_literals + +import pytest + +from pre_commit_hooks.string_fixer import main + +TESTS = ( + # Base cases + ( + "''", + "''", + 0 + ), + ( + '""', + "''", + 1 + ), + ( + r'"\'"', + r'"\'"', + 0 + ), + ( + r'"\""', + r'"\""', + 0 + ), + ( + r"'\"\"'", + r"'\"\"'", + 0 + ), + # String somewhere in the line + ( + 'x = "foo"', + "x = 'foo'", + 1 + ), + # Test escaped characters + ( + r'"\'"', + r'"\'"', + 0 + ), + # Docstring + ( + '""" Foo """', + '""" Foo """', + 0 + ), + # Fuck it, won't even try to fix + ( + """ + x = " \\n + foo \\n + "\n + """, + """ + x = " \\n + foo \\n + "\n + """, + 0 + ), +) + + +@pytest.mark.parametrize(('input_s', 'expected_output', 'expected_retval'), TESTS) +def test_rewrite(input_s, expected_output, expected_retval, tmpdir): + tmpfile = tmpdir.join('file.txt') + + with open(tmpfile.strpath, 'w') as f: + f.write(input_s) + + retval = main([tmpfile.strpath]) + assert tmpfile.read() == expected_output + assert retval == expected_retval