diff --git a/src/flynt/code_editor.py b/src/flynt/code_editor.py index c08bf93..96872e4 100644 --- a/src/flynt/code_editor.py +++ b/src/flynt/code_editor.py @@ -20,6 +20,7 @@ from flynt.utils.utils import ( apply_unicode_escape_map, contains_comment, + preserve_escaped_newlines, unicode_escape_map, ) @@ -176,6 +177,7 @@ def try_chunk(self, chunk: AstChunk) -> None: if changed and escape_map and not is_raw: converted = apply_unicode_escape_map(converted, escape_map) if changed: + converted = preserve_escaped_newlines(snippet, converted) contract_lines = chunk.n_lines - 1 if contract_lines == 0: line = self.src_lines[chunk.start_line] @@ -218,7 +220,9 @@ def maybe_replace( - self._byte_to_char_idx(chunk.start_line, chunk.start_idx) for line in lines ) - converted = converted.replace("\\n", "\n") + snippet_text = self.code_in_chunk(chunk) + if "\\n" not in snippet_text: + converted = converted.replace("\\n", "\n") else: lines_fit = len( f"{converted}{rest}" diff --git a/src/flynt/utils/utils.py b/src/flynt/utils/utils.py index 50ac9ac..795b8d7 100644 --- a/src/flynt/utils/utils.py +++ b/src/flynt/utils/utils.py @@ -209,3 +209,30 @@ def repl(match: re.Match[str]) -> str: def contains_unicode_escape(code: str) -> bool: """Return ``True`` if ``code`` contains unicode or octal escape sequences.""" return bool(unicode_escape_re.search(code)) + + +def preserve_escaped_newlines(original: str, converted: str) -> str: + """Restore backslash newline escapes lost during AST transformation.""" + orig_lines = original.splitlines() + conv_lines = converted.splitlines() + if not orig_lines or not conv_lines: + return converted + if orig_lines[0].rstrip().endswith("\\") and not conv_lines[0].rstrip().endswith( + "\\" + ): + m = re.match(r"(\s*[furbFURB]*[\'\"]{3})", conv_lines[0]) + if m: + prefix = m.group(1) + rest = conv_lines[0][len(prefix) :].lstrip() + if len(orig_lines) > 1: + indent = len(orig_lines[1]) - len(orig_lines[1].lstrip()) + else: + indent = len(prefix) - len(prefix.lstrip()) + conv_lines[0] = prefix + "\\" + if rest: + conv_lines.insert(1, " " * indent + rest) + for idx, line in enumerate(orig_lines[1:], start=1): + if line.strip() == "\\": + indent = len(line) - len(line.lstrip()) + conv_lines.insert(idx, " " * indent + "\\") + return "\n".join(conv_lines) diff --git a/test/integration/expected_out_single_line/escaped_newline.py b/test/integration/expected_out_single_line/escaped_newline.py new file mode 100644 index 0000000..2ea1e53 --- /dev/null +++ b/test/integration/expected_out_single_line/escaped_newline.py @@ -0,0 +1,31 @@ +from textwrap import dedent + +def f(): + arg = "text" + return dedent( + """\ + \ + some {} + lorem ipsum + """.format(arg) + ) + + +def f_extra(): + arg = "text" + return """\ + some {}\n""".format(arg) + + +def f_multiple(): + arg = "text" + return """\ + \ + \ + some {}\n""".format(arg) + + +def f_single_quotes(): + arg = "text" + return '''\ + some {}\n'''.format(arg) diff --git a/test/integration/test_issue83.py b/test/integration/test_issue83.py index 72d0eb0..4984ee8 100644 --- a/test/integration/test_issue83.py +++ b/test/integration/test_issue83.py @@ -1,11 +1,9 @@ -import pytest from functools import partial from test.integration.utils import try_on_file from flynt.code_editor import fstringify_code_by_line -@pytest.mark.xfail(reason="newline escapes lost, issue #83") def test_escaped_newline(state): out, expected = try_on_file( "escaped_newline.py", diff --git a/test/integration/utils.py b/test/integration/utils.py index 2f6e06b..06e96d5 100644 --- a/test/integration/utils.py +++ b/test/integration/utils.py @@ -6,7 +6,6 @@ EXCLUDED = { "bom.py", "class.py", - "escaped_newline.py", # not supported yet, #83 on github "multiline_limit.py", } samples = {p.name for p in (int_test_path / "samples_in").glob("*.py")} - EXCLUDED