Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

### Fixed

- Fix `string()` dropping a leading newline of a multiline string on round-trip: a value beginning with a newline is now rendered with an extra leading newline (the one the parser trims after the opening delimiter) so it survives re-parsing.
- Fix invalid serialization with a duplicated comma when removing a non-edge element from a parsed inline table. ([#486](https://github.com/python-poetry/tomlkit/pull/486))
- Fix invalid serialization with a duplicated comma when appending or inserting into a comma-first formatted array. ([#499](https://github.com/python-poetry/tomlkit/pull/499))
- Fix `ParseError` when a sub-table extends the last element of an array of tables after an unrelated table. ([#261](https://github.com/python-poetry/tomlkit/issues/261))
Expand Down
26 changes: 25 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ def test_create_super_table_with_aot() -> None:
({}, "My String\x12", '"My String\\u0012"'),
({}, "My String\x7f", '"My String\\u007f"'),
({"escape": False}, "My String\u0001", '"My String\u0001"'),
({"multiline": True}, "\nMy\nString\n", '"""\nMy\nString\n"""'),
({"multiline": True}, "\nMy\nString\n", '"""\n\nMy\nString\n"""'),
({"multiline": True}, 'My"String', '"""My"String"""'),
({"multiline": True}, 'My""String', '"""My""String"""'),
({"multiline": True}, 'My"""String', '"""My""\\"String"""'),
Expand Down Expand Up @@ -537,6 +537,30 @@ def test_create_string(kwargs: dict[str, Any], example: str, expected: str) -> N
assert value.as_string() == expected


@pytest.mark.parametrize(
"kwargs, example",
[
({"multiline": True}, "\n"),
({"multiline": True}, "\nMy\nString\n"),
({"multiline": True}, "\r\nMy\nString"),
({"multiline": True, "literal": True}, "\nMy\nString"),
({"multiline": True, "literal": True}, "\r\nMy\nString"),
],
)
def test_create_multiline_string_with_leading_newline_round_trips(
kwargs: dict[str, Any], example: str
) -> None:
"""A multiline string whose value starts with a newline must survive a
round-trip. The parser trims a newline immediately following the opening
delimiter, so the rendered form has to account for it (otherwise the
leading newline is silently dropped when the output is parsed again).
"""
value = tomlkit.string(example, **kwargs)
assert str(value) == example
reparsed = parse(f"k = {value.as_string()}\n")["k"]
assert str(reparsed) == example


@pytest.mark.parametrize(
"kwargs, example",
[
Expand Down
7 changes: 7 additions & 0 deletions tomlkit/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -2251,6 +2251,13 @@ def from_raw(
escaped = type_.escaped_sequences
string_value = escape_string(value, escaped) if escape and escaped else value

if 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.
string_value = "\n" + string_value

return cls(type_, decode(value), string_value, Trivia())


Expand Down
Loading