Keep keyed children when their wrapper container goes away - #70
Merged
maartenbreddels merged 2 commits intoSep 29, 2026
Merged
Conversation
The previous fuzz extension moves a keyed child out of a container that
is replaced by one of another type. Review found the same problem when
the container is removed instead, e.g.
VBox(children=[VBox(children=[child]) if wrapped else child]): the
removal of the stale wrapper removes the child with it, although the
child is still in the tree.
The random trees now have an Unwrapper component that puts a child with
an explicit key inside 0, 1 or 2 wrapper containers, depending on its
state, with a child that is sometimes the same object (use_memo) and
sometimes new. The moved keyed children (of Mover and Unwrapper) now
have an effect, and the effects that run at close() are compared too,
so a kept child whose effect was cleaned up (or never is) shows up.
On master, 2 of the 12 seeds fail (73 of 300): the default renderer
raises AttributeError ('NoneType' object has no attribute 'comm_id'),
and the fast renderer leaves elements behind. Fixed in the next commit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
When a container is no longer in the new tree, the stale-key sweep
removes it with all its children, by key. An explicit key is the same
in any container of a component, so a keyed child that is still in the
tree at another level, e.g. in
VBox(children=[VBox(children=[child]) if wrapped else child]), was
removed too: the default renderer raised AttributeError ('NoneType'
object has no attribute 'comm_id'), the fast renderer showed a closed
widget or left elements behind.
This is the same problem that the previous fix solved for a container
replaced by one of another type. The stale-key sweeps (both renderers,
and the fast renderer's root sweep) now remove through the same path,
renamed from _remove_replaced to _remove_outgoing: children with an
explicit key that the new tree still uses stay, and reconciliation
updates them where the new tree has them. The fast renderer cleans up
the effects of stale subtrees early (before new effects run); that
pass now also leaves the kept children alone, so their effects keep
running. Shared elements keep the old behavior, and close() still
removes everything.
The fuzz test: on master 73 of 300 seeds fail; with this fix 1000 of
1000 pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
When a container is no longer in the new tree, the stale-key sweep removes it together with all its children, by key. An explicit
.key()is the same in any container of a component. So a keyed child that is still in the tree at another level was removed too. For example:When
wrappedbecomes False:AttributeError: 'NoneType' object has no attribute 'comm_id'This is the same kind of bug that #65 fixed for a container that is replaced by one of another type. The crossreview of #65 found this case, where the container is removed.
What
Two commits:
Unwrappercomponent puts a child with an explicit key inside 0, 1 or 2 wrapper containers, depending on its state. The child is sometimes the same object (made once withuse_memo) and sometimes new. The keyed children that move (inMoverandUnwrapper) now have an effect, and the effect log atclose()is compared too. On 1.10.4, 2 of the 12 seeds fail, and 73 of 300._remove_replacedto_remove_outgoing. Children with an explicit key that the new tree still uses stay, and reconciliation updates them where the new tree has them.close()still removes everything.Visible change: a keyed child whose wrapper goes away now keeps its widget and its state.
Tests
test_keyed_child_out_of_removed_wrapper(child as the same object, and as a new object): fails on 1.10.4 in both renderers.test_keyed_child_out_of_removed_wrapper_keeps_its_effects: the effect runs once and is cleaned up once, atclose().pytest reacton/withREACTON_FAST=0and=1: all pass.Review
Crossreview by three reviewers (astra, opus, glm), two rounds.
self.contextowns the removed element at each sweepused_keysis the right "still in the tree" signalclose()still cleans up everything.shared()elements have several bugs, and they fail the same way on master.🤖 Generated with Claude Code