Skip to content

Write emptied AoT fallback under its declaring header - #693

Open
ryanchou1994 wants to merge 1 commit into
python-poetry:masterfrom
ryanchou1994:fix/emptied-aot-out-of-order-header
Open

ryanchou1994 wants to merge 1 commit into
python-poetry:masterfrom
ryanchou1994:fix/emptied-aot-out-of-order-header

Conversation

@ryanchou1994

Copy link
Copy Markdown

Summary

Since #567, an emptied array of tables is rendered as key = [] at the top of the nearest header scope. If a table on its path is declared by a header in a later fragment ([[a.b.c]] ... [a], or [a.b]), the hoisted dotted key a.b.c = [] declares that table before its header does, which is invalid TOML (both tomlkit and tomllib reject the output).

This change only affects rendering in tomlkit/container.py: before writing a fallback, _declaring_table looks for the deepest table on its path that emits a header in the current scope. If there is one, the fallback is handed to that table and written at the top of its own scope with a relative key, e.g.

[a]
b.c = []
y = 1

Fallbacks with no declaring header are written exactly as before, and parsing is untouched.

Tests: the new parametrized cases (5 shapes, also checked with tomllib on 3.11+) and the exact-output test fail on master and pass with the change. Full suite including the toml-test submodule on Linux (Python 3.14): 1223 passed. The 7 tmp_path errors in test_toml_file.py come from the container user having no name and happen on master too. ruff check and ruff format --check are clean.

Fixes #690

Duplicate Check

Agent Drafting Metadata

  • Agent: Claude Code
  • Model: Claude Opus 5.5
  • Notes: The fix and tests were written with Claude Code, and I decided to submit it. Before opening the PR it was re-run in a clean Linux container as described above, and two extra test shapes were added after a review pass.

An emptied array of tables is rendered as `key = []` at the top of the
nearest header scope. When a table on its path is declared by a header
in a later fragment (`[[a.b.c]]` ... `[a]`), the hoisted dotted key
`a.b.c = []` declared `a` before `[a]`, which is invalid TOML.

Pass such fallbacks down to the deepest table whose header declares
them, and write them there with a relative key. Fallbacks without a
declaring header are rendered as before.

Fixes python-poetry#690
Copilot AI balanced review requested due to automatic review settings November 2, 2026 05:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@Ekko82 Ekko82 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked 0c0b3867 on Windows with Python 3.13.9. All 98 tests in tests/test_toml_document.py pass. The six added regression cases fail against base d7def7d and pass on this head; the five existing emptied-AoT cases pass on both.

Twelve additional parse/pop/dump cases also pass, covering equivalent quoted keys, deeper declaring headers, multiple emptied siblings, nested array elements, and controls without a declaring header or with a nonempty AoT. Both tomllib and tomlkit recover the mutated document's data, and rendering is unchanged after a second call or a deepcopy. Nine of these cases produce invalid TOML on the base.

@lexingtonhibiki

Copy link
Copy Markdown

One header-order case still produces invalid TOML on 0c0b3867:

import tomlkit
import tomllib

source = "[a.b]\nz=1\n[a]\ny=1\n[[a.b.c]]\nx=1\n"
tomllib.loads(source)  # valid input

doc = tomlkit.parse(source)
doc["a"]["b"]["c"].pop()
rendered = doc.as_string()
print(rendered)
tomllib.loads(rendered)

The output is:

[a.b]
z=1
[a]
b.c = []
y=1

tomllib rejects this with Cannot redefine namespace ('a', 'b'); tomlkit also rejects it. The c = [] fallback needs to go under the earlier [a.b] header. In this shape that header lives in a different a fragment from the emptied AoT, while the _scope_aots() call for the [a] fragment only searches that fragment's body.

This is an uncovered case, not a regression introduced by the PR: it also fails on base d7def7d. I checked five standalone parse/pop/dump cases on Python 3.12.14: the original PR case and two controls pass on this head; this case and a deeper variant with a separate declaring-header fragment still fail. Repeated rendering and deepcopy retain the same output. I have not run the full upstream suite. This was AI-assisted verification, not human review.

@ryanchou1994

Copy link
Copy Markdown
Author

Thanks for the repro. I can reproduce it on 0c0b386 and on the base too, so it's the same fallback with the declaring [a.b] header sitting in an earlier fragment of a. I'll extend the lookup to cover that, add this case and the deeper variant to the tests, and push here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Emptying a nested array of tables before an out-of-order super-table renders invalid TOML (0.15.2, since #567)

4 participants