fix: give deferred singleShot callbacks a receiver context
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 1m2s
CI / test (3.12) (pull_request) Successful in 1m8s
CI / test (3.14) (pull_request) Successful in 1m7s
CI / test (3.13) (pull_request) Successful in 1m16s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m21s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m32s
CI / test-with-coverage (pull_request) Successful in 1m37s
CI / coverage-analysis (pull_request) Successful in 3s
CI / lint (push) Skipped
CI / test (3.12) (push) Skipped
CI / test (3.13) (push) Skipped
CI / test-with-beamline-plugins (pxi_bec) (push) Skipped
CI / test-with-beamline-plugins (pxii_bec) (push) Skipped
CI / test-with-beamline-plugins (pxiii_bec) (push) Skipped
CI / lint (pull_request) Successful in 1m2s
CI / test (3.12) (pull_request) Successful in 1m8s
CI / test (3.14) (pull_request) Successful in 1m7s
CI / test (3.13) (pull_request) Successful in 1m16s
CI / test-with-beamline-plugins (pxi_bec) (pull_request) Successful in 1m14s
CI / test-with-beamline-plugins (pxii_bec) (pull_request) Successful in 1m21s
CI / test-with-beamline-plugins (pxiii_bec) (pull_request) Successful in 1m32s
CI / test-with-coverage (pull_request) Successful in 1m37s
CI / coverage-analysis (pull_request) Successful in 3s
Root cause of the long-standing cross-test SystemError flake
("QPushButton returned NULL", CI failing test_title_label after an
unrelated file): TitleLabel schedules
QTimer.singleShot(0, self._apply_collapsed) at construction; when the
banner is deleted before the event loop spins (what every panel test
does), the pending timer fires into the dead C++ object and poisons
whatever test runs next. Deterministic repro added as a regression
test. The receiver-context overload singleShot(0, self, ...) auto-
cancels the callback when the widget dies. Same fix for the two
deferred main-window callbacks (dock split, floating-dock enlarge).
Note for the record: the LOCAL segfault storm this afternoon was the
dev machine's disk hitting 100% full (uv cache at 15G) — Bus errors
from mmap on a full disk, unrelated to any code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2865,7 +2865,7 @@ class MainWindow(QMainWindow):
|
||||
if not self._default_dock_split_done:
|
||||
self._default_dock_split_done = True
|
||||
if not self.state_manager.settings.value("main_window/state"):
|
||||
QTimer.singleShot(0, self._apply_default_dock_split)
|
||||
QTimer.singleShot(0, self, self._apply_default_dock_split)
|
||||
|
||||
def _apply_default_dock_split(self) -> None:
|
||||
self.resizeDocks(
|
||||
@@ -3022,7 +3022,7 @@ class MainWindow(QMainWindow):
|
||||
# Pop-outs open enlarged instead of keeping the cramped docked
|
||||
# size. Deferred: the window is mid-reparent while the signal
|
||||
# fires.
|
||||
QTimer.singleShot(0, lambda d=dock: self._enlarge_floating_dock(d))
|
||||
QTimer.singleShot(0, dock, lambda d=dock: self._enlarge_floating_dock(d))
|
||||
|
||||
def _enlarge_floating_dock(self, dock: QDockWidget) -> None:
|
||||
if not dock.isFloating():
|
||||
|
||||
@@ -89,8 +89,12 @@ class TitleLabel(QLabel):
|
||||
if settings.value(self._settings_key, default_collapsed, type=bool):
|
||||
self._collapsed = True
|
||||
# Deferred: the panel adds its other widgets after constructing
|
||||
# the TitleLabel, so siblings don't exist yet.
|
||||
QTimer.singleShot(0, self._apply_collapsed)
|
||||
# the TitleLabel, so siblings don't exist yet. `self` as the
|
||||
# receiver context, or the pending timer outlives a deleted
|
||||
# banner and fires into the dead C++ object — the source of the
|
||||
# long-standing "QPushButton returned NULL" SystemError flake
|
||||
# poisoning whatever test runs next.
|
||||
QTimer.singleShot(0, self, self._apply_collapsed)
|
||||
|
||||
def paintEvent(self, event):
|
||||
# QSS has no text-shadow, so paint by hand: the QSS background box
|
||||
|
||||
@@ -73,3 +73,19 @@ def test_not_collapsible_by_default(qtbot):
|
||||
title = TitleLabel("Plain", panel)
|
||||
grid.addWidget(title, 0, 0)
|
||||
assert not hasattr(title, "toggle_button")
|
||||
|
||||
|
||||
def test_pending_collapse_timer_dies_with_the_banner():
|
||||
"""The deferred _apply_collapsed must not fire into a deleted banner —
|
||||
that was the historic "QPushButton returned NULL" / SystemError flake
|
||||
poisoning whichever test ran next (singleShot without receiver context
|
||||
outlives the widget)."""
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
_remove_key()
|
||||
host = QWidget()
|
||||
TitleLabel("DoomedBanner", host, collapsible=True, default_collapsed=True)
|
||||
# Python-owned host: del cascades the C++ delete to the banner while
|
||||
# its singleShot(0) is still pending.
|
||||
del host
|
||||
QApplication.processEvents() # must not raise into the event loop
|
||||
|
||||
Reference in New Issue
Block a user