From 4aa821b8d2712e0595ac0a0842bf9c953c6a9d38 Mon Sep 17 00:00:00 2001 From: Anshul Singh <72524975+ekanshul@users.noreply.github.com> Date: Thu, 20 Aug 2026 02:42:56 +0530 Subject: [PATCH] fix: do not shift a backslash continuation line onto the previous row When a docstring edit shifts the lines that follow it, _do_update_token_indices() decides whether a token stays on the previous token's row by comparing its (not yet shifted) start row with the (already shifted) end row of the previous token. A backslash continuation emits no NL token between the two physical lines, so for the first token of the continuation line that comparison is satisfied by coincidence whenever the shift is one row, the token is put on the previous token's row, and tokenize.untokenize() raises "start (r,c) precedes previous end (r,c)". Treat a token whose previous token's physical line ends with a backslash as starting a new row, which is what the tokenizer guarantees. Fixes #377 --- src/docformatter/format.py | 10 ++++++++-- tests/_data/string_files/do_format_code.toml | 11 +++++++++++ tests/formatter/test_do_format_code.py | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/docformatter/format.py b/src/docformatter/format.py index 584ff6a..21d8e62 100644 --- a/src/docformatter/format.py +++ b/src/docformatter/format.py @@ -196,8 +196,14 @@ def _do_update_token_indices( is_multiline = _is_multiline_parameter(tokens, i - 1) is_same_line = tokens[i].line == tokens[i - 1].line is_same_position = tokens[i].start[0] == tokens[i - 1].end[0] - - if ( + # 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. + prev_line = tokens[i - 1].line.rstrip("\r\n") + is_backslash_continuation = not is_same_line and prev_line.endswith("\\") + + if not is_backslash_continuation and ( is_multiline or (is_same_line or is_same_position) and tokens[i - 1].type diff --git a/tests/_data/string_files/do_format_code.toml b/tests/_data/string_files/do_format_code.toml index d652a15..6928cfe 100644 --- a/tests/_data/string_files/do_format_code.toml +++ b/tests/_data/string_files/do_format_code.toml @@ -1260,3 +1260,14 @@ expected="def foo():\n \"\"\"Summary.\"\"\"\n x = 1\n # next line has 4 [issue_360_no_trailing_newline] source="def foo():\n \"\"\"\n Hello foo.\n \"\"\"\n x = 1" expected="def foo():\n \"\"\"Hello foo.\"\"\"\n x = 1" + +# A backslash continuation after a docstring that gets a blank line inserted +# must not crash the tokenizer round-trip: the continuation line has no NL +# token in front of it, so its row must be derived from the previous token. +[issue_377_backslash_continuation] +source="class A:\n \"\"\"Doc.\"\"\"\n x = 1 \\\n + 2\n" +expected="class A:\n \"\"\"Doc.\"\"\"\n\n x = 1 \\\n + 2\n" + +[issue_377_module_docstring] +source="\"\"\"Doc.\"\"\"\nx = 1 \\\n + 2\n" +expected="\"\"\"Doc.\"\"\"\n\nx = 1 \\\n + 2\n" diff --git a/tests/formatter/test_do_format_code.py b/tests/formatter/test_do_format_code.py index 037ace1..b506e52 100644 --- a/tests/formatter/test_do_format_code.py +++ b/tests/formatter/test_do_format_code.py @@ -143,6 +143,8 @@ ("issue_331_black_module_docstring", ["--black", ""]), ("issue_355", NO_ARGS), ("issue_360_no_trailing_newline", NO_ARGS), + ("issue_377_backslash_continuation", NO_ARGS), + ("issue_377_module_docstring", NO_ARGS), ], ) def test_do_format_code(test_key, test_args, args):