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
50 changes: 50 additions & 0 deletions tests/test_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,40 @@ def test_array_add_line_invalid_value() -> None:
assert len(t) == 0


def test_table_and_aot_in_array_are_rejected() -> None:
# A table renders as its own ``[header]`` block, so accepting one in an
# array silently drops the header on rendering while ``unwrap()`` still
# reports the element: the array looks fine and the text is not valid TOML.
table = api.table()
table.append("a", 1)
aot = parse("[[a]]\nx = 1\n\n[[a]]\nx = 2\n")["a"]

t = api.array()
t.append(1)

with pytest.raises(ValueError, match="cannot contain a table"):
t.append(table)
with pytest.raises(ValueError, match="cannot contain a table"):
t.insert(0, table)
with pytest.raises(ValueError, match="cannot contain a table"):
t[0] = table
with pytest.raises(ValueError, match="cannot contain a table"):
t.add_line(table)

with pytest.raises(ValueError, match="cannot contain an array of tables"):
t.append(aot)
with pytest.raises(ValueError, match="cannot contain an array of tables"):
t.insert(0, aot)
with pytest.raises(ValueError, match="cannot contain an array of tables"):
t[0] = aot
with pytest.raises(ValueError, match="cannot contain an array of tables"):
t.add_line(aot)

# None of the rejected conversions may have touched the array.
assert t.as_string() == "[1]"
assert t.unwrap() == [1]


def test_dicts_are_converted_to_tables_and_keep_order() -> None:
t = item(
{
Expand Down Expand Up @@ -964,6 +998,22 @@ def test_append_table_to_inline_table_raises() -> None:
inline_table["table"] = table


def test_append_aot_to_inline_table_raises() -> None:
# An array of tables renders as its own ``[[header]]`` blocks, so it needs
# the same rejection as a bare table: accepting it drops the headers on
# rendering while ``unwrap()`` still reports the tables.
aot = parse("[[a]]\nx = 1\n\n[[a]]\nx = 2\n")["a"]
inline_table = api.inline_table()

with pytest.raises(ValueError, match="cannot contain an array of tables"):
inline_table.append("aot", aot)
with pytest.raises(ValueError, match="cannot contain an array of tables"):
inline_table["aot"] = aot

assert inline_table.as_string() == "{}"
assert inline_table.unwrap() == {}


def test_deleting_inline_table_element_does_not_leave_trailing_separator() -> None:
table = api.inline_table()
table["foo"] = "bar"
Expand Down
15 changes: 15 additions & 0 deletions tomlkit/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -1526,6 +1526,7 @@ def add_line(
it = item(el, _parent=self)
if isinstance(it, Comment) or (add_comma and isinstance(el, Whitespace)):
raise ValueError(f"item type {type(it)} is not allowed in add_line")
self._validate_child(it)
if not isinstance(it, Whitespace):
if whitespace:
new_values.append(Whitespace(whitespace))
Expand Down Expand Up @@ -1583,17 +1584,29 @@ def item(self, index: int) -> Item:
def __getitem__(self, key: int | slice) -> Any: # type: ignore[override]
return list.__getitem__(self, key)

def _validate_child(self, value: Any) -> None:
# A table or an array of tables renders as its own ``[header]`` block,
# so it cannot be rendered inside the brackets of an array. Accepting
# one would drop the header on rendering -- the element is still
# reported by ``unwrap()``, but the text is no longer valid TOML.
if isinstance(value, Table):
raise ValueError("Arrays cannot contain a table")
if isinstance(value, AoT):
raise ValueError("Arrays cannot contain an array of tables")

def __setitem__(self, key: int | slice, value: Any) -> None: # type: ignore[override]
if isinstance(key, slice):
raise ValueError("slice assignment is not supported")
it = item(value, _parent=self)
self._validate_child(it)
list.__setitem__(self, key, it)
if key < 0:
key += len(self)
self._value[self._index_map[key]].value = it

def insert(self, pos: int, value: Any) -> None: # type: ignore[override]
it = item(value, _parent=self)
self._validate_child(it)
length = len(self)
if not isinstance(it, (Comment, Whitespace)):
list.insert(self, pos, it)
Expand Down Expand Up @@ -2051,6 +2064,8 @@ def append(self, key: Key | str | None, _item: Any) -> InlineTable:
def _validate_child(self, _item: Item) -> None:
if isinstance(_item, Table):
raise ValueError("Inline tables cannot contain a table")
if isinstance(_item, AoT):
raise ValueError("Inline tables cannot contain an array of tables")

def as_string(self) -> str:
buf = "{"
Expand Down
Loading