Skip to content

Fix Array.__delitem__ corrupting rendered output on slice deletion - #599

Open
Fries-tempura wants to merge 1 commit into
python-poetry:masterfrom
Fries-tempura:fix-array-slice-delitem
Open

Fries-tempura wants to merge 1 commit into
python-poetry:masterfrom
Fries-tempura:fix-array-slice-delitem

Conversation

@Fries-tempura

Copy link
Copy Markdown

Summary

Array.__delitem__ keeps the in-memory list and the rendered value groups in sync, but for slice deletions it recomputes which rendered positions to drop with:

range(key.start or 0, key.stop or length, key.step or 1)

This mishandles ordinary slices:

  • stop == 0 (a[:0]): 0 or length evaluates to length, so a no-op deletion removes every rendered element.
  • negative bounds aren't normalized: a[-1:]range(-1, length), and a[:-1]range(0, -1) (empty, removes nothing).

list.__delitem__ handles the slice correctly, so the list contents stay right, but as_string() / dumps() silently disagree with them:

import tomlkit
doc = tomlkit.parse("a = [1, 2, 3]\n")
del doc["a"][:0]        # deletes nothing
tomlkit.dumps(doc)      # 'a = []\n'  — expected 'a = [1, 2, 3]\n'
operation list(arr) arr.as_string() expected
del arr[:0] [1, 2, 3] [] [1, 2, 3]
del arr[-1:] [1, 2] [] [1, 2]
del arr[:-1] [3] [1, 2, 3] [3]

Fix: use slice.indices(length), which resolves None, negative, and out-of-range bounds exactly the way list.__delitem__ does, so the removed render positions always match the list.

Tests: added test_array_slice_deletion_keeps_render_in_sync covering a[:0], a[-1:], and a[:-1]. Full tests/test_items.py passes (87 passed).

Agent Drafting Metadata

  • Agent: Claude Code (property-based testing workflow)
  • Model: Claude Opus 5
  • Notes: The bug was found by property-based testing (Hypothesis) run against main. The one-line fix and the regression test were drafted with the agent and verified locally — the repro fails before the change and tests/test_items.py passes (87 passed) after. Reviewed by the submitter before opening.

@Jalst Jalst 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.

LGTM! Verified the fix and tested for regressions.

Analysis & Root Cause

The previous implementation:

range(key.start or 0, key.stop or length, key.step or 1)

suffered from two issues:

  1. For stop == 0 (e.g. del a[:0]), 0 or length evaluated to length, mistakenly wiping out all rendered items from self._value.
  2. Negative indices were passed directly into self._index_map, which only indexes non-negative positions, causing key misses and state desynchronization between the underlying list and rendered self._value.

Using standard-library key.indices(length) correctly resolves None, negative offsets, and bounds according to Python list slice semantics. Furthermore, because indices_to_remove is sorted in reverse order (reverse=True) and self._index_map is strictly monotonic, deleting higher indices first guarantees that lower _value offsets remain valid throughout the deletion loop before self._reindex() is invoked.

Verification

  • Ran full test suite: 1,053 passed.
  • Tested 500 randomized slice permutations (varying negative/positive bounds, steps > 1, negative steps, empty slices, and slices past array boundaries) across single-line and multiline arrays with inline comments. In all cases, a.as_string() round-trips and agrees with list(a).
  • Clean diff with zero side-effects on existing formatting logic.

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.

2 participants