From 8eacf5bed9786facc9bcdc21dc096b89d0c14cb0 Mon Sep 17 00:00:00 2001 From: Kartik Kenchi Date: Wed, 24 Jun 2026 11:29:29 +0530 Subject: [PATCH] prefix every line of a multiline comment in Item.comment and add_line --- tests/test_items.py | 27 +++++++++++++++++++++++++++ tomlkit/items.py | 24 +++++++++++++++++++----- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/test_items.py b/tests/test_items.py index b785a5e..15891a4 100644 --- a/tests/test_items.py +++ b/tests/test_items.py @@ -515,6 +515,17 @@ def test_array_add_line() -> None: ) +def test_array_add_line_multiline_comment_is_valid_toml() -> None: + t = api.array() + t.add_line(1, 2, 3, comment="first line\nsecond line") + t.add_line(indent="") + doc = api.document() + doc["a"] = t + rendered = doc.as_string() + assert "\nsecond line" not in rendered + assert parse(rendered).as_string() == rendered + + def test_array_add_line_invalid_value() -> None: t = api.array() with pytest.raises(ValueError, match="is not allowed"): @@ -887,6 +898,22 @@ def test_trim_comments_when_building_inline_table() -> None: assert table.as_string() == '{foo = "bar", baz = "foobaz"}' +def test_comment_method_multiline_is_valid_toml() -> None: + doc = api.document() + doc["x"] = 1 + doc["x"].comment("first line\nsecond line") + rendered = doc.as_string() + assert rendered == "x = 1 # first line\n# second line\n" + assert parse(rendered).as_string() == rendered + + +def test_comment_method_keeps_existing_hash_prefix() -> None: + doc = api.document() + doc["x"] = 1 + doc["x"].comment("# already a comment") + assert doc.as_string() == "x = 1 # already a comment\n" + + def test_append_table_to_inline_table_raises() -> None: table = api.table() table.append("a", 1) diff --git a/tomlkit/items.py b/tomlkit/items.py index e9fdaff..3b2516d 100644 --- a/tomlkit/items.py +++ b/tomlkit/items.py @@ -232,6 +232,21 @@ def item(value: Any, _parent: Item | None = None, _sort_keys: bool = False) -> I raise ConvertError(f"Unable to convert an object of {type(value)} to a TOML item") +def _format_comment(comment: str) -> str: + """Prefix every line of ``comment`` with ``#`` so a multi-line comment + stays valid TOML. + + Only the first line was prefixed before, so the second line onward of a + comment with embedded newlines rendered without a ``#`` and produced + output that no longer re-parses. A line that already starts with ``#`` is + left untouched and an empty line becomes a bare ``#``. + """ + return "\n".join( + line if line.lstrip().startswith("#") else f"# {line}" if line else "#" + for line in comment.split("\n") + ) + + class StringType(Enum): # Single Line Basic SLB = '"' @@ -500,11 +515,8 @@ def unwrap(self) -> Any: def comment(self, comment: str) -> Item: """Attach a comment to this item""" - if not comment.strip().startswith("#"): - comment = "# " + comment - self._trivia.comment_ws = " " - self._trivia.comment = comment + self._trivia.comment = _format_comment(comment) return self @@ -1550,7 +1562,9 @@ def add_line( if comment: indent = " " if items else "" new_values.append( - Comment(Trivia(indent=indent, comment=f"# {comment}", trail="")) + Comment( + Trivia(indent=indent, comment=_format_comment(comment), trail="") + ) ) list.extend(self, data_values) if len(self._value) > 0: