Guard move() against moving an element into its own descendant - #6180
Conversation
DateInput applied .props('no-parent-event') to the inner QDate, but
Quasar 2.18.5's QDate has no such prop (noParentEvent belongs to the
anchor-props mixin behind QMenu/QTooltip). It was an inert no-op
attribute; removing it changes no behavior (open/close is driven by the
parent QMenu).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Moving an element into itself or one of its own descendants left both elements referencing each other, so descendants(), clear() and str() would recurse infinitely. Reject such a move before mutating the tree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@falkoschindler asked me to bring the open PRs up to date with The conflict is in Assigning you to make the ownership visible. Generated by Claude Code |
Reconcile the cycle guard with zauberzeug#6176: `target_slot` is still resolved and validated before the element is detached, and the descendant check now runs ahead of that resolution, so neither an invalid slot nor a descendant target can mutate the tree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without this, the test passes even when the descendant guard sits after the detach — the exact mis-merge this branch had to avoid. Mirrors the wording of the sibling check added in zauberzeug#6176. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Posted by Claude Code on evnchn's behalf. Resolved — merged How the two move() changes combinedYou were right that this one was a judgement call rather than a mechanical merge. #6176 stops The ordering is the load-bearing part. The cycle guard has to run before parent_slot = self.parent_slot
assert parent_slot is not None
if target_container is not None and self in target_container.ancestors(include_self=True):
raise ValueError('Cannot move an element into itself or one of its descendants.')
target_container = target_container or parent_slot.parentThe naive merge — guard placed after the detach — still passes the original test, because that test only checked that root = a.parent_slot
with pytest.raises(ValueError):
a.move(b)
with pytest.raises(ValueError):
a.move(a)
assert a in root.children, 'a rejected move must keep the element in its original slot'Verified: |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
falkoschindler
left a comment
There was a problem hiding this comment.
Thanks for catching this, Evan! The guard is correct and well-placed: it runs before any mutation, so a rejected move leaves the tree untouched, and checking the target's ancestors is O(depth) rather than scanning the whole subtree.
I pushed a small polish commit: the guard now sits below the defaulting line (the default target — the current parent — can never be self or a descendant, so the is not None check became unnecessary), the test pins the expected error with match= to distinguish it from the unknown-slot ValueError, and the test variables are named outer/inner/label/other to spell out the nesting.
Motivation
move()has no guard against moving a container into itself or one of its own descendants, which forms ana ↔ bcontainment cycle. Any later tree traversal (descendants(),str(),clear(),delete()) then recurses forever →RecursionError/ hang.Minimal reproduction (
nicegui/element.py:524) — recursion capped first so it can't hang the machine:→
RecursionErrorImplementation
Reject a move into self or a descendant with a
ValueErrorbefore any mutation (mirrors the existing target-slot validation), so the tree is left intact; legitimate moves are unaffected.Verification
origin/main(no error raised, cycle formed), passes with the fix; a legitimatex.move(p2)still works../nicegui· pylint 10.00/10 · pytest (touched) — all green.Progress