Skip to content

Commit be8e031

Browse files
larsonerclaude
authored andcommitted
test: deterministically tear down example widgets
The namespace from runpy.run_path is discarded, so widgets created by an example are garbage-collected at an arbitrary later point during event processing. A still-visible widget whose C++ side is deleted mid-event-loop can receive a paintEvent during deletion, which PyQt5 escalates to a process abort: RuntimeError: wrapped C/C++ object of type QRangeSlider has been deleted as seen intermittently on the windows-latest (3.12) pyqt5 job in test_example[demo_widgets/range_slider.py] (e.g. the scheduled main run on 2026-08-22, and both pyapp-kit#741 and pyapp-kit#742). Close (hide) all top-level widgets first so nothing can paint, then deleteLater() + sendPostedEvents(None, DeferredDelete) to destroy the native widgets (and stop their timers) while hidden -- note that processEvents() alone does NOT dispatch DeferredDelete events -- then gc.collect() for anything left over. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 34d322d commit be8e031

1 file changed

Lines changed: 23 additions & 0 deletions

File tree

‎tests/test_examples.py‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import gc
12
import runpy
23
from pathlib import Path
34
from unittest.mock import patch
45

56
import pytest
7+
from qtpy.QtCore import QCoreApplication, QEvent
68
from qtpy.QtWidgets import QApplication
79

810
EXAMPLES_DIR = Path(__file__).parent.parent / "docs" / "examples"
@@ -34,3 +36,24 @@ def test_example(qapp: QApplication, example: Path) -> None:
3436
# dependency it requires, that's fine
3537
pytest.xfail(str(e))
3638
raise
39+
finally:
40+
# Deterministically tear down whatever the example created. The
41+
# namespace returned by runpy is discarded, so the example's
42+
# widgets are otherwise garbage-collected at an arbitrary later
43+
# point -- and a still-visible widget whose C++ side is deleted
44+
# during event processing can receive a paintEvent mid-deletion,
45+
# which aborts the process on Windows/PyQt5 with
46+
# "RuntimeError: wrapped C/C++ object ... has been deleted"
47+
# (seen intermittently in superqt's QRangeSlider paintEvent for
48+
# demo_widgets/range_slider.py). Hide everything first so
49+
# nothing paints, then destroy the native widgets while they are
50+
# hidden (which also stops their timers), then collect whatever
51+
# is left while flushing events.
52+
for widget in qapp.topLevelWidgets():
53+
widget.close()
54+
widget.deleteLater()
55+
# NB: processEvents() does not dispatch DeferredDelete events
56+
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
57+
qapp.processEvents()
58+
gc.collect()
59+
qapp.processEvents()

0 commit comments

Comments
 (0)