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
40 changes: 40 additions & 0 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,46 @@ def test_build_table_with_dotted_key() -> None:
}


def test_replace_dotted_key_table_does_not_capture_following_dotted_key() -> None:
content = """\
a.b = 1
c.d = 2
"""
doc = parse(content)

doc["a"] = {}

expected = """\
[a]

[c]
d = 2
"""
assert doc.as_string() == expected
assert parse(doc.as_string()) == {"a": {}, "c": {"d": 2}}


def test_replace_nested_dotted_key_table_keeps_following_dotted_key() -> None:
content = """\
[a]
b.c = 1
d.e = 2
"""
doc = parse(content)

doc["a"]["b"] = {}

expected = """\
[a]
[a.b]

[a.d]
e = 2
"""
assert doc.as_string() == expected
assert parse(doc.as_string()) == {"a": {"b": {}, "d": {"e": 2}}}


def test_parse_subtables_no_extra_indent() -> None:
expected = """\
[a]
Expand Down
93 changes: 74 additions & 19 deletions tomlkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,16 +572,24 @@ def last_item(self) -> Item | None:
def as_string(self) -> str:
"""Render as TOML string."""
s = ""
scope_is_root = True
for k, v in self._body:
if k is not None:
if isinstance(v, Table):
force_table_header = (
not scope_is_root and self._renders_dotted_items(v)
)
if (
s.strip(" ")
and not s.strip(" ").endswith("\n")
and "\n" not in v.trivia.indent
):
s += "\n"
s += self._render_table(k, v)
s += self._render_table(
k, v, force_table_header, dotted_scope_valid=scope_is_root
)
if self._renders_table_header(k, v, force_table_header):
scope_is_root = False
elif isinstance(v, AoT):
if (
s.strip(" ")
Expand All @@ -590,26 +598,20 @@ def as_string(self) -> str:
):
s += "\n"
s += self._render_aot(k, v)
scope_is_root = False
else:
s += self._render_simple_item(k, v)
else:
s += self._render_simple_item(k, v)

return s

def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> str:
cur = ""

if table.display_name is not None:
_key = table.display_name
else:
_key = key.as_string()

if prefix is not None:
_key = prefix + "." + _key

if (
not table.is_super_table()
def _renders_table_header(
self, key: Key, table: Table, force: bool = False
) -> bool:
return (
force
or not table.is_super_table()
or (
any(
not isinstance(v, (Table, AoT, Whitespace, Null))
Expand All @@ -625,7 +627,44 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st
)
and not key.is_dotted()
)
):
)

def _renders_dotted_items(self, table: Table) -> bool:
if not table.is_super_table():
return False

for k, v in table.value.body:
if isinstance(v, Table):
if self._renders_dotted_items(v):
return True
elif isinstance(v, AoT):
continue
elif k is not None and not isinstance(v, (Whitespace, Null)):
return True

return False

def _render_table(
self,
key: Key,
table: Table,
force_header: bool = False,
prefix: str | None = None,
dotted_scope_valid: bool = True,
) -> str:
cur = ""

if table.display_name is not None:
_key = table.display_name
else:
_key = key.as_string()

if prefix is not None:
_key = prefix + "." + _key

scope_is_current = self._renders_table_header(key, table, force_header)
dotted_scope_valid = dotted_scope_valid or scope_is_current
if scope_is_current:
open_, close = "[", "]"
if table.is_aot_element():
open_, close = "[[", "]]"
Expand All @@ -648,6 +687,9 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st

for k, v in table.value.body:
if isinstance(v, Table):
force_child_header = (
not dotted_scope_valid and self._renders_dotted_items(v)
)
if (
cur.strip(" ")
and not cur.strip(" ").endswith("\n")
Expand All @@ -656,13 +698,23 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st
cur += "\n"
assert k is not None
if v.is_super_table():
if k.is_dotted() and not key.is_dotted():
if k.is_dotted() and not key.is_dotted() and not force_child_header:
# Dotted key inside table
cur += self._render_table(k, v)
cur += self._render_table(
k, v, dotted_scope_valid=dotted_scope_valid
)
else:
cur += self._render_table(k, v, prefix=_key)
cur += self._render_table(
k,
v,
force_child_header,
prefix=_key,
dotted_scope_valid=dotted_scope_valid,
)
else:
cur += self._render_table(k, v, prefix=_key)
if self._renders_table_header(k, v, force_child_header):
dotted_scope_valid = False
elif isinstance(v, AoT):
if (
cur.strip(" ")
Expand All @@ -672,9 +724,12 @@ def _render_table(self, key: Key, table: Table, prefix: str | None = None) -> st
cur += "\n"
assert k is not None
cur += self._render_aot(k, v, prefix=_key)
dotted_scope_valid = False
else:
cur += self._render_simple_item(
k, v, prefix=_key if key.is_dotted() else None
k,
v,
prefix=_key if key.is_dotted() and not scope_is_current else None,
)

return cur
Expand Down
Loading