Summary
Tomlkit 0.15.1 rejects a valid document when a table contains an out-of-order
child, its concrete parent is declared afterward, and another table separates a
later sibling child.
This is a regression: tomllib and Tomlkit 0.13.3, 0.14.0, and 0.15.0 all
accept the document.
Minimal reproduction
import tomlkit
source = """\
[tool.ruff]
[tool.ruff.lint.a]
[tool.ruff.lint]
[[tool.poetry.source]]
[tool.ruff.lint.b]
"""
tomlkit.loads(source)
Actual result
tomlkit.exceptions.ParseError: Key "lint" already exists. at line 5 col 0
Expected result
The document parses successfully, matching tomllib:
{
"tool": {
"ruff": {"lint": {"a": {}, "b": {}}},
"poetry": {"source": [{}]},
}
}
Each header in the minimized example is necessary to trigger the failure. The
array-of-tables is not special: replacing [[tool.poetry.source]] with any
intervening table header also reproduces it, while removing the intervening
header makes it pass.
Regression boundary
Reproduced on Python 3.13.8 and 3.14.2:
- stdlib
tomllib: passes
- Tomlkit 0.13.3: passes
- Tomlkit 0.14.0: passes
- Tomlkit 0.15.0: passes
- Tomlkit 0.15.1 and current
master: fail
Suspected root cause
The regression appears to come from the concrete+super validation added in
#530 / commit d3c76f0.
When [tool.ruff.lint.b] is appended, the existing lint entry is represented
by OutOfOrderTableProxy, while the candidate is a Table.
_validate_table_candidate() checks:
isinstance(existing, (Table, AoT)) != isinstance(v, (Table, AoT))
The proxy is table-like but fails the left-hand isinstance check, so valid
input is reported as a type conflict and raises KeyAlreadyPresent("lint").
Summary
Tomlkit 0.15.1 rejects a valid document when a table contains an out-of-order
child, its concrete parent is declared afterward, and another table separates a
later sibling child.
This is a regression:
tomlliband Tomlkit 0.13.3, 0.14.0, and 0.15.0 allaccept the document.
Minimal reproduction
Actual result
Expected result
The document parses successfully, matching
tomllib:{ "tool": { "ruff": {"lint": {"a": {}, "b": {}}}, "poetry": {"source": [{}]}, } }Each header in the minimized example is necessary to trigger the failure. The
array-of-tables is not special: replacing
[[tool.poetry.source]]with anyintervening table header also reproduces it, while removing the intervening
header makes it pass.
Regression boundary
Reproduced on Python 3.13.8 and 3.14.2:
tomllib: passesmaster: failSuspected root cause
The regression appears to come from the concrete+super validation added in
#530 / commit d3c76f0.
When
[tool.ruff.lint.b]is appended, the existinglintentry is representedby
OutOfOrderTableProxy, while the candidate is aTable._validate_table_candidate()checks:The proxy is table-like but fails the left-hand
isinstancecheck, so validinput is reported as a type conflict and raises
KeyAlreadyPresent("lint").