Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/docformatter/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,14 @@ def _do_update_token_indices(
is_same_line = tokens[i].line == tokens[i - 1].line
is_same_position = tokens[i].start[0] == _prev_end_row
_prev_end_row = tokens[i].end[0]
# A backslash continuation joins two physical lines without an NL
# token between them, so the current token always starts on the row
# after the previous one, even though the rows may look the same once
# the previous token has been shifted.
# A backslash continuation between tokens joins two physical lines without
# an NL token. A multiline string can start on the previous token's row
# and contain that backslash itself, so compare the original row positions
# before treating it as a continuation between tokens.
prev_line = tokens[i - 1].line.rstrip("\r\n")
is_backslash_continuation = not is_same_line and prev_line.endswith("\\")
is_backslash_continuation = (
not is_same_position and not is_same_line and prev_line.endswith("\\")
)

if not is_backslash_continuation and (
is_multiline
Expand Down
58 changes: 58 additions & 0 deletions tests/formatter/test_continued_string_preservation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Preserve escaped physical newlines inside ordinary string tokens."""

import ast
import sys

import pytest

from docformatter import Formatter


@pytest.mark.integration
@pytest.mark.parametrize("args", [[""], ["--black", ""]])
@pytest.mark.parametrize("newline", ["\n", "\r\n"])
@pytest.mark.parametrize(
"source",
[
'value = "first\\\nsecond"\n',
"value = 'first\\\nsecond'\n",
'value = r"first\\\nsecond"\n',
'value = b"first\\\nsecond"\n',
'value = u"first\\\nsecond"\n',
'print("first\\\nsecond")\n',
'def value():\n return "first\\\nsecond"\n',
'value = "first\\\nsecond\\\nthird"\n',
'value = "first\\\nsecond" "suffix"\n',
'value = "first\\\nsecond" + "suffix"\n',
'value = "first\\\nsecond"',
'value = (\n "first\\\nsecond"\n)\n',
],
)
def test_continued_string_is_preserved(source, newline, test_args, args):
"""A continuation within a string must not duplicate surrounding Python code."""
source = source.replace("\n", newline)
formatter = Formatter(test_args, sys.stderr, sys.stdin, sys.stdout)

formatted = formatter._do_format_code(source)

assert formatted == source
assert ast.dump(ast.parse(formatted)) == ast.dump(ast.parse(source))
assert formatter._do_format_code(formatted) == formatted


@pytest.mark.integration
@pytest.mark.parametrize("args", [[""], ["--black", ""]])
@pytest.mark.parametrize("newline", ["\n", "\r\n"])
def test_continued_string_after_docstring_rewrite(newline, test_args, args):
"""Preserve string-token positions when docstring formatting shifts later rows."""
source = 'class Example:\n """Summary."""\n value = "first\\\nsecond"\n'
expected = source.replace('"""Summary."""\n', '"""Summary."""\n\n')
source = source.replace("\n", newline)
expected = expected.replace("\n", newline)
formatter = Formatter(test_args, sys.stderr, sys.stdin, sys.stdout)

formatted = formatter._do_format_code(source)

assert formatted == expected
assert ast.dump(ast.parse(formatted)) == ast.dump(ast.parse(source))
assert formatter._do_format_code(formatted) == formatted
Loading