diff --git a/docs/index.rst b/docs/index.rst index d65a1b3..bac3aa6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,6 +51,16 @@ A note about handling text literals mark bytestrings with ``b''`` and native strings in ``str('')`` or something similar that survives the transformation. +Line endings +============ + +Normally, changed files are written with the usual line endings for the platform +that python-modernize is run on (LF for Unix / Mac OS X, or CRLF for Windows). + +The ``--unix-line-endings`` option writes Unix line endings regardless of +the curent platform. Similarly, the ``--windows-line-endings`` option +writes Windows line endings regardless of the current platform. + Indices and tables ////////////////// diff --git a/libmodernize/main.py b/libmodernize/main.py index 4fe934c..f752f07 100644 --- a/libmodernize/main.py +++ b/libmodernize/main.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, print_function +import io import sys import logging import optparse @@ -17,6 +18,28 @@ from libmodernize import __version__ from libmodernize.fixes import lib2to3_fix_names, six_fix_names, opt_in_fix_names + +class LineEndingsRefactoringTool(StdoutRefactoringTool): + '''2to3 refactoring tool that rewrites files with specified line endings''' + def __init__(self, *args, **kwargs): + self.newline = kwargs.pop('newline', None) + super(LineEndingsRefactoringTool, self).__init__(*args, **kwargs) + + def write_file(self, new_text, filename, old_text, encoding): + super(LineEndingsRefactoringTool, self).write_file(new_text, filename, + old_text, encoding) + + if self.newline is not None: + self.log_debug("Rewriting %s with line endings %r", + filename, self.newline) + with io.open(filename, 'r', encoding=encoding) as f: + contents = f.read() + + with io.open(filename, 'w', encoding=encoding, + newline=self.newline) as f: + f.write(contents) + + usage = __doc__ + """\ %s @@ -63,6 +86,10 @@ def main(args=None): "(only useful for Python 2.6+).") parser.add_option("--no-six", action="store_true", default=False, help="Exclude fixes that depend on the six package.") + parser.add_option("--unix-line-endings", action="store_true", default=False, + help="Write files with Unix (LF) line endings.") + parser.add_option("--windows-line-endings", action="store_true", default=False, + help="Write files with Windows (CRLF) line endings.") fixer_pkg = 'libmodernize.fixes' avail_fixes = set(refactor.get_fixers_from_package(fixer_pkg)) @@ -82,6 +109,10 @@ def main(args=None): print(fixname) if not args: return 0 + if options.unix_line_endings and options.windows_line_endings: + print("--unix-line-endings and --windows-line-endings are mutually exclusive", + file=sys.stderr) + return 2 if not args: print("At least one file or directory argument required.", file=sys.stderr) print("Use --help to show usage.", file=sys.stderr) @@ -125,8 +156,17 @@ def main(args=None): else: requested = default_fixes fixer_names = requested.difference(unwanted_fixes) - rt = StdoutRefactoringTool(sorted(fixer_names), flags, sorted(explicit), - options.nobackups, not options.no_diffs) + + if options.unix_line_endings: + newline = '\n' + elif options.windows_line_endings: + newline = '\r\n' + else: + newline = None + + rt = LineEndingsRefactoringTool(sorted(fixer_names), flags, sorted(explicit), + options.nobackups, not options.no_diffs, + newline=newline) # Refactor all files and directories passed as arguments if not rt.errors: diff --git a/tests/test_newlines.py b/tests/test_newlines.py new file mode 100644 index 0000000..fc8e8de --- /dev/null +++ b/tests/test_newlines.py @@ -0,0 +1,34 @@ +from __future__ import absolute_import + +import os +from utils import check_on_input, expect_error + +TESTCASE = ("""\ +# sample code +isinstance(x, basestring) +""", """\ +# sample code +from __future__ import absolute_import +import six +isinstance(x, six.string_types) +""") + + +def test_to_native_line_endings(): + foreign_linesep = '\r\n' if (os.linesep == '\n') else '\n' + check_on_input(TESTCASE[0], TESTCASE[1].replace('\n', os.linesep), + write_newline=foreign_linesep, read_newline='') + +def test_windows_to_unix_line_endings(): + check_on_input(TESTCASE[0], TESTCASE[1], + extra_flags=['--unix-line-endings'], + write_newline='\r\n', read_newline='') + +def test_unix_to_windows_line_endings(): + check_on_input(TESTCASE[0], TESTCASE[1].replace('\n', '\r\n'), + extra_flags=['--windows-line-endings'], + write_newline='\n', read_newline='') + +def test_options_conflict(): + expect_error(TESTCASE[0], + extra_flags=['--unix-line-endings', '--windows-line-endings']) diff --git a/tests/utils.py b/tests/utils.py index ed82bd3..14badef 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,13 +1,18 @@ from __future__ import absolute_import +import io import os.path import tempfile import shutil +import sys + +PY3 = sys.version_info[0] >= 3 from libmodernize.main import main as modernize_main -def check_on_input(input_content, expected_content, extra_flags = []): +def check_on_input(input_content, expected_content, extra_flags = [], + write_newline=None, read_newline=None): """ Check that input_content is fixed to expected_content, idempotently. @@ -16,17 +21,21 @@ def check_on_input(input_content, expected_content, extra_flags = []): matches expected_content. Then, runs modernize again with any extra arguments, and asserts that the second run makes no changes. """ + if not PY3 and isinstance(input_content, bytes): + # Allow native strings as input on Python 2 + input_content = input_content.decode('ascii') + tmpdirname = tempfile.mkdtemp() try: test_input_name = os.path.join(tmpdirname, "input.py") - with open(test_input_name, "wt") as input_file: + with io.open(test_input_name, "wt", newline=write_newline) as input_file: input_file.write(input_content) def _check(this_input_content, which_check): modernize_main(extra_flags + ["-w", test_input_name]) output_content = "" - with open(test_input_name, "rt") as output_file: + with io.open(test_input_name, "rt", newline=read_newline) as output_file: for line in output_file: if line: output_content += line @@ -40,3 +49,16 @@ def _check(this_input_content, which_check): _check(expected_content, "idempotence check failed") finally: shutil.rmtree(tmpdirname) + +def expect_error(input_content, extra_flags=[]): + tmpdirname = tempfile.mkdtemp() + try: + test_input_name = os.path.join(tmpdirname, "input.py") + with open(test_input_name, "wt") as input_file: + input_file.write(input_content) + + ret = modernize_main(extra_flags + ["-w", test_input_name]) + if ret == 0: + raise AssertionError("didn't expect to succeed") + finally: + shutil.rmtree(tmpdirname)