diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index ade38ac7..e91d5dde 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -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(): diff --git a/src/aare/gui/widgets/title_label.py b/src/aare/gui/widgets/title_label.py index 6c5d5c28..0abc91fa 100644 --- a/src/aare/gui/widgets/title_label.py +++ b/src/aare/gui/widgets/title_label.py @@ -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 diff --git a/tests/unit/gui/test_title_label.py b/tests/unit/gui/test_title_label.py index 168dc31d..daf48ecf 100644 --- a/tests/unit/gui/test_title_label.py +++ b/tests/unit/gui/test_title_label.py @@ -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