From 739d8bc8481d19110a4c29e133772a58604ed2be Mon Sep 17 00:00:00 2001 From: Shivam Maurya Date: Wed, 14 Oct 2026 08:05:41 +0530 Subject: [PATCH] fix: honour escape=False for a leading newline in multiline strings PR #551 re-added the newline trimmed by the parser for any multiline string whose value starts with one, but did so regardless of the escape flag. With escape=False the caller owns the raw body, so the extra newline made it impossible to render such a value as written. Gate the compensation on escape so the raw body is emitted verbatim when escaping is disabled, restoring the pre-#551 behaviour for that case. Fixes #655 --- tests/test_api.py | 23 +++++++++++++++++++++++ tomlkit/items.py | 6 +++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 741f8956..4b878ad1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -632,6 +632,29 @@ def test_create_multiline_string_with_leading_newline_round_trips( assert str(reparsed) == example +def test_create_multiline_literal_string_with_leading_newline_no_escape() -> None: + """A multiline literal string created with ``escape=False`` whose value + starts with a newline must render verbatim, without a doubled newline + (#655). PR #551 re-added the trimmed leading newline unconditionally, which + made it impossible to dump such a value as written. + """ + value = tomlkit.string( + "\n aligned\n strings\n", + multiline=True, + literal=True, + escape=False, + ) + assert tomlkit.dumps(value) == "'''\n aligned\n strings\n'''" + assert str(value) == "\n aligned\n strings\n" + + +def test_create_multiline_basic_string_with_leading_newline_no_escape() -> None: + """Same as above for a multiline basic string built with ``escape=False``.""" + value = tomlkit.string("\nfoo\n", multiline=True, escape=False) + assert tomlkit.dumps(value) == '"""\nfoo\n"""' + assert str(value) == "\nfoo\n" + + @pytest.mark.parametrize( "kwargs, example", [ diff --git a/tomlkit/items.py b/tomlkit/items.py index 26b01097..4b5b3738 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -2311,11 +2311,15 @@ def from_raw( if type_ is StringType.MLB and escape: string_value = _BARE_CR.sub(r"\\r", string_value) - if type_.is_multiline() and string_value[:1] in ("\n", "\r"): + if escape and type_.is_multiline() and string_value[:1] in ("\n", "\r"): # A newline immediately following the opening delimiter of a # multiline string is trimmed by the parser, so a value that # starts with a newline would otherwise lose it on round-trip. # Emit an extra leading newline (the trimmed one) to preserve it. + # + # This only applies when escaping is enabled: with escape=False the + # caller is responsible for the raw body, so re-adding a newline + # would make it impossible to render a leading newline as written. string_value = "\n" + string_value return cls(type_, decode(value), string_value, Trivia())