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 @@ -23,6 +23,7 @@
- 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 unparseable serialization when adding a key to a dotted-key table inside an inline table. ([#500](https://github.com/python-poetry/tomlkit/pull/500))
- Fix a table replaced by a plain value being serialized inside the preceding table's body when other tables follow; the value now moves before the first table like other root-level values. ([#504](https://github.com/python-poetry/tomlkit/issues/504))
- Fix assigning a table over a dotted key (e.g. `doc["a"] = {...}` where `a` came from `a.b = ...`): the dotted prefix was duplicated onto the new `[a]` header, and the header then swallowed any sibling that follows it on round-trip. The replacement now renders as a plain table and, when needed, moves before the inline entries (values and dotted keys) it would otherwise capture. ([#513](https://github.com/python-poetry/tomlkit/issues/513), [#524](https://github.com/python-poetry/tomlkit/issues/524))
- Restore `dumps()` rendering mapping-like wrappers around a parsed document (e.g. `dotty_dict`'s `Dotty`) through their delegated `as_string`, preserving the original table order and layout instead of re-encoding through a plain dict — a 0.15.0 regression. ([#482](https://github.com/python-poetry/tomlkit/issues/482))
- Fix uncontrolled recursion when parsing deeply nested documents: crafted input could crash the process with a `RecursionError`. Values nested more than 100 levels deep and keys with more than 100 dotted fragments now raise `ParseError`. ([#459](https://github.com/python-poetry/tomlkit/issues/459))
- Fix `comment()` producing invalid TOML for a multiline string by prefixing every line with `#`, not just the first. ([#449](https://github.com/python-poetry/tomlkit/issues/449))
Expand Down
69 changes: 69 additions & 0 deletions tests/test_toml_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,75 @@ def test_replace_with_aot_of_nested() -> None:
assert doc.as_string().strip() == dedent(expected).strip()


def test_replace_dotted_key_with_table() -> None:
# https://github.com/python-poetry/tomlkit/issues/524
content = "fruit.apple = true\n"
doc = parse(content)
doc["fruit"] = {"a": 1}
# The dotted prefix must be dropped instead of duplicated onto the header.
assert (
doc.as_string()
== """[fruit]
a = 1
"""
)
assert parse(doc.as_string())["fruit"] == {"a": 1}


def test_replace_dotted_key_with_empty_table_keeps_following_sibling() -> None:
# https://github.com/python-poetry/tomlkit/issues/513
content = """a.b = 1
c.d = 2
"""
doc = parse(content)
doc["a"] = {}
# ``[a]`` must not swallow the following ``c.d`` dotted key.
assert (
doc.as_string()
== """c.d = 2

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


def test_replace_dotted_key_with_table_keeps_following_sibling() -> None:
# https://github.com/python-poetry/tomlkit/issues/513
content = """a.b = 1
c.d = 2
"""
doc = parse(content)
doc["a"] = {"x": 9}
assert (
doc.as_string()
== """c.d = 2

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


def test_replace_value_with_table_keeps_following_dotted_sibling() -> None:
# A plain value turning into a table must likewise clear the inline region
# (including dotted keys) before emitting its header.
content = """x = 1
c.d = 2
"""
doc = parse(content)
doc["x"] = {}
assert (
doc.as_string()
== """c.d = 2

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


def test_replace_with_comment() -> None:
content = 'a = "1"'
doc = parse(content)
Expand Down
35 changes: 32 additions & 3 deletions tomlkit/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,10 +821,30 @@ def _replace_at(

k, v = self._body[idx]
assert k is not None
# A dotted key renders its value inline (e.g. ``a.b = 1``), which is only
# consistent with a super table. When the replacement value renders with
# its own ``[header]`` instead (a non-super table), keeping the dotted key
# duplicates the prefix onto the header (#524). Drop the dotted key so the
# replacement renders as a plain table.
dotted_to_header = (
k.is_dotted() and isinstance(value, Table) and not value.is_super_table()
)
# That new header also captures every sibling that renders inline -- plain
# values and dotted keys -- if any still follow it (#513), so it must be
# moved past them, exactly as a value-to-table change already is.
reposition_dotted = dotted_to_header and any(
not isinstance(cur_val, (Null, Whitespace))
and not (
isinstance(cur_val, (Table, AoT))
and (cur_key is None or not cur_key.is_dotted())
)
for cur_key, cur_val in self._body[idx + 1 :]
)
if not isinstance(new_key, Key):
if (
isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
or new_key != k.key
or dotted_to_header
):
new_key = SingleKey(new_key)
else: # Inherit the sep of the old key
Expand All @@ -835,12 +855,21 @@ def _replace_at(
if new_key != k:
dict.__delitem__(self, k.key)

if isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table)):
if (
isinstance(value, (AoT, Table)) != isinstance(v, (AoT, Table))
or reposition_dotted
):
self.remove(k)
if isinstance(value, (AoT, Table)):
# new tables should appear after all non-table values
# New tables must appear after all entries that render inline:
# plain values and dotted keys (which are super tables). Skip
# those and insert before the first real ``[header]`` so the new
# table cannot swallow a following sibling on round-trip.
for i in range(idx, len(self._body)):
if isinstance(self._body[i][1], (AoT, Table)):
cur_key, cur_val = self._body[i]
if isinstance(cur_val, (AoT, Table)) and not (
cur_key is not None and cur_key.is_dotted()
):
self._insert_at(i, new_key, value)
idx = i
break
Expand Down
Loading