From 9916d5f090000ccde418709c9125d7848ba63b00 Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Sat, 8 Aug 2026 23:55:51 +0300 Subject: [PATCH] fix: do not add a line continuation to a file without a trailing newline The synthetic NEWLINE token that tokenize emits for a source without a trailing newline has line='', so the strip() guard added in #360 stopped counting it as a continuation and untokenize bridged the row gap with a backslash, corrupting the file. Exclude only blank-but-non-empty lines instead. Signed-off-by: Eljees <3.14hell@gmail.com> --- src/docformatter/classify.py | 23 +++++++++++++++++++- tests/_data/string_files/do_format_code.toml | 9 ++++++++ tests/formatter/test_do_format_code.py | 1 + 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/docformatter/classify.py b/src/docformatter/classify.py index ec54f51..9b8b553 100644 --- a/src/docformatter/classify.py +++ b/src/docformatter/classify.py @@ -469,7 +469,7 @@ def is_newline_continuation( """ if ( token.type in (tokenize.NEWLINE, tokenize.NL) - and token.line.strip() + and not _is_blank_line(token.line) and token.line.strip() in prev_token.line.strip() ): return True @@ -477,6 +477,27 @@ def is_newline_continuation( return False +def _is_blank_line(line: str) -> bool: + """Determine if a physical line is blank. + + A blank line is a non-empty line that holds nothing but whitespace, e.g. + "\\n" or " \\n". The empty string is *not* a blank line; tokenize uses + it as the line of the NEWLINE token it synthesizes for source that has no + trailing newline, as well as for DEDENT and ENDMARKER tokens. + + Parameters + ---------- + line : str + The physical line the token was read from; tokenize.TokenInfo.line. + + Returns + ------- + bool + True if the line holds only whitespace and is not empty. + """ + return bool(line) and not line.strip() + + def is_string_variable( token: tokenize.TokenInfo, prev_token: tokenize.TokenInfo, diff --git a/tests/_data/string_files/do_format_code.toml b/tests/_data/string_files/do_format_code.toml index b573fb8..d652a15 100644 --- a/tests/_data/string_files/do_format_code.toml +++ b/tests/_data/string_files/do_format_code.toml @@ -1251,3 +1251,12 @@ pass [issue_355] source="def foo():\n \"\"\"Summary.\"\"\"\n x = 1\n # next line has 4 spaces of trailing whitespace\n \n return x\n" expected="def foo():\n \"\"\"Summary.\"\"\"\n x = 1\n # next line has 4 spaces of trailing whitespace\n \n return x\n" + +# Source that does not end with a newline. tokenize synthesizes a NEWLINE +# token for it whose line attribute is the empty string (CPython < 3.12); that +# token must still be treated as part of the preceding line, otherwise +# untokenize bridges the row gap with a backslash continuation and the +# formatted file no longer parses. +[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" diff --git a/tests/formatter/test_do_format_code.py b/tests/formatter/test_do_format_code.py index 2d4e479..037ace1 100644 --- a/tests/formatter/test_do_format_code.py +++ b/tests/formatter/test_do_format_code.py @@ -142,6 +142,7 @@ ("do_not_break_f_string_single_quotes", NO_ARGS), ("issue_331_black_module_docstring", ["--black", ""]), ("issue_355", NO_ARGS), + ("issue_360_no_trailing_newline", NO_ARGS), ], ) def test_do_format_code(test_key, test_args, args):