Skip to content
Closed
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
45 changes: 45 additions & 0 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -1804,3 +1804,48 @@ def test_emptied_array_of_tables_is_relative_to_the_nearest_header() -> None:
doc["t"]["u"]["a"].pop()

assert doc.as_string() == "[t]\nu.a = []\n"


def test_delete_last_dotted_key_in_inline_table_keeps_valid_toml() -> None:
# https://github.com/python-poetry/tomlkit/issues/662
# Removing the only child of a dotted key in an inline table left a
# dangling separator, producing invalid TOML like ``t = {a="", , d=1}``.
doc = parse('t = {a="", b.c=1, d=1}\n')
del doc["t"]["b"]["c"]

assert doc.as_string() == 't = {a="", b={}, d=1}\n'
tomllib = pytest.importorskip("tomllib")
assert tomllib.loads(doc.as_string()) == {"t": {"a": "", "b": {}, "d": 1}}


def test_delete_last_dotted_key_in_inline_table_key_positions() -> None:
# https://github.com/python-poetry/tomlkit/issues/662
# The empty slot used to surface as a leading or trailing comma depending
# on where the dotted key sat, and dropped ``b`` entirely when it was the
# only key.
tomllib = pytest.importorskip("tomllib")

doc = parse("t = {a=1, b.c=1}\n")
del doc["t"]["b"]["c"]
assert doc.as_string() == "t = {a=1, b={}}\n"
tomllib.loads(doc.as_string())

doc = parse("t = {b.c=1, a=1}\n")
del doc["t"]["b"]["c"]
assert doc.as_string() == "t = {b={}, a=1}\n"
tomllib.loads(doc.as_string())

doc = parse("t = {b.c=1}\n")
del doc["t"]["b"]["c"]
assert doc.as_string() == "t = {b={}}\n"
tomllib.loads(doc.as_string())


def test_delete_last_nested_dotted_key_in_inline_table_keeps_valid_toml() -> None:
# https://github.com/python-poetry/tomlkit/issues/662
doc = parse('t = {a="", b.c.d=1, e=1}\n')
del doc["t"]["b"]["c"]["d"]

assert doc.as_string() == 't = {a="", b.c={}, e=1}\n'
tomllib = pytest.importorskip("tomllib")
assert tomllib.loads(doc.as_string()) == {"t": {"a": "", "b": {"c": {}}, "e": 1}}
17 changes: 15 additions & 2 deletions tomlkit/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -2182,7 +2182,14 @@ def as_string(self) -> str:
# A table materialized from a dotted key renders one
# `prefix.child = value` pair per child, so that keys added
# after parsing stay attached to the dotted prefix.
rendered = ", ".join(self._render_dotted(k, v))
dotted = self._render_dotted(k, v)
if dotted:
rendered = ", ".join(dotted)
else:
# Every child of the dotted key was removed: keep the
# now-empty table rather than emitting nothing, which
# would leave a dangling separator and invalid TOML.
rendered = f"{k.as_string()}{k.sep or '='}{{}}"
else:
rendered = (
f"{k.as_string() + ('.' if k.is_dotted() else '')}"
Expand Down Expand Up @@ -2220,7 +2227,13 @@ def _render_dotted(self, key: Key, table: Table) -> list[str]:
if k is None:
continue
if isinstance(v, Table):
parts.extend(f"{prefix}{sub}" for sub in self._render_dotted(k, v))
sub = self._render_dotted(k, v)
if sub:
parts.extend(f"{prefix}{s}" for s in sub)
else:
# A dotted child whose keys were all removed renders as
# an empty inline table so the output stays valid TOML.
parts.append(f"{prefix}{k.as_string()}{k.sep or '='}{{}}")
else:
trail = v.trivia.trail.replace("\n", "")
parts.append(
Expand Down
Loading