Skip to content

Fix the PySide wrapper-recycling GUI test flake#26

Merged
jacobson30-bot merged 1 commit into
mainfrom
fix-wrapper-recycling-flake
Jun 11, 2026
Merged

Fix the PySide wrapper-recycling GUI test flake#26
jacobson30-bot merged 1 commit into
mainfrom
fix-wrapper-recycling-flake

Conversation

@jacobson30-bot

Copy link
Copy Markdown
Contributor

Summary

Root-causes and fixes the intermittent 'QGraphicsItemGroup' object has no attribute 'connect'/'triggered' failure (5 CI hits today, always test_gui_processing_panel.py).

Mechanism: QGraphicsItem is not a QObject — when a scene dies C++-side, Shiboken never invalidates its items' Python wrappers. A wrapper outliving its C++ item leaves a dangling pointer binding in Shiboken's cache; a later C++ allocation (the QAction that QMenu.addAction creates) reusing that address resurrects the stale wrapper as the wrong type.

Fix, two layers:

  1. ImageCanvas.destroyed hook drops every scene-item reference before children are deleted (destroyed fires first), so item wrappers unregister their bindings while the C++ items are still alive — even when the canvas wrapper itself is still referenced (a deleteLater()'d dialog still held somewhere).
  2. The conftest GUI drain adds a second gc.collect() after deferred deletions, collecting wrappers that were invalidated (not garbage) at the first collect — previously they were freed at a nondeterministic point inside a later test, exactly while it allocated onto recycled addresses.

Test plan

  • tests/test_canvas_item_lifetime.py: the key test fails without the hook (verified by stash), passes with it; hook doesn't leak the canvas; plain teardown unaffected.
  • 5× soak of the flaky module locally, full suite 2334 passed.
  • Honest caveat: the flake never reproduced locally — CI frequency over the coming runs is the real verdict; the regression test guards the mechanism regardless.

🤖 Generated with Claude Code

…g flake

The intermittent CI failure ("'QGraphicsItemGroup' object has no attribute
'connect'/'triggered'" inside QMenu.addAction, 5 hits today across PR and
main runs, always in test_gui_processing_panel.py) is Shiboken wrapper
recycling: QGraphicsItem is not a QObject, so when a canvas's scene is
destroyed C++-side, Shiboken never invalidates the Python wrappers for its
items. A wrapper that outlives its C++ item keeps a dangling pointer binding
in Shiboken's cache, and when a later C++ allocation (the QAction QMenu
creates) reuses that heap address, the stale wrapper is resurrected as the
wrong type.

Two layers:

- ImageCanvas connects its destroyed signal to a hook that drops every
  scene-item reference (_roi_items, markers, selection/preview/pixmap/
  overlay items). QObject.destroyed fires before children are deleted, so
  the item wrappers deallocate — unregistering their bindings — while the
  C++ items are still alive. The closure captures the attribute dict, not
  self, so it cannot keep the view alive. This holds even in the hazardous
  case where the canvas *wrapper* is still referenced from Python while its
  C++ object dies (a deleteLater()'d dialog still held somewhere).

- The conftest GUI-test drain gains a second gc.collect AFTER deferred
  deletions: step 4 destroys C++ objects whose wrappers were not garbage at
  the first collect; Shiboken invalidates those QObject wrappers but their
  __dict__ (often holding item wrappers) survived until a nondeterministic
  collection inside a LATER test — exactly while that test allocates new Qt
  objects onto recycled addresses.

tests/test_canvas_item_lifetime.py pins the mechanism: C++ destruction with
the canvas wrapper still referenced must release every item wrapper (fails
without the hook), the hook must not leak the canvas, and plain Python
teardown is unaffected. The flake never reproduced locally, so CI over the
coming runs is the real verdict; the regression test guards the mechanism
either way.

Co-Authored-By: Claude Fable 5 <[email protected]>
@jacobson30-bot jacobson30-bot merged commit 1d395b1 into main Jun 11, 2026
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.

1 participant