Fix PRINT_REDIRECTOR calling into a destroyed LogViewer#593
Fix PRINT_REDIRECTOR calling into a destroyed LogViewer#593markomarkovic wants to merge 1 commit into
Conversation
PRINT_REDIRECTOR is a module-level singleton, so the lambda connected to sigStdoutWrite in prepare_panes accumulated one connection per MainWindow and was never disconnected. Each lambda also closed over self and resolved self.components["log"] late through a dict that MainMixin holds as a class attribute, so it reached whichever LogViewer registered last. Once a LogViewer was destroyed, the next print() called into a deleted C++ object and raised RuntimeError. Connecting the bound method instead lets PyQt drop the connection when the receiver is destroyed.
dde7a06 to
10a2eb0
Compare
|
@markomarkovic What are the steps to reproduce this error? I am not seeing it, even in CI. |
|
Fair — it doesn't fail the suite as it stands, and that's really the point: the connection is dangling, but nothing currently exercises it. Two things hide it:
Deterministic repro on master ( import sys
from PyQt5 import sip
from PyQt5.QtWidgets import QMessageBox
from cq_editor.__main__ import MainWindow
from cq_editor.main_window import PRINT_REDIRECTOR
def test_stray_output_after_log_destroyed(qtbot, mocker):
mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes)
mocker.patch.object(QMessageBox, "warning", return_value=QMessageBox.Discard)
win = MainWindow()
qtbot.addWidget(win)
sip.delete(win.components["log"]) # the window's LogViewer goes away
errors = []
original, sys.excepthook = sys.excepthook, lambda t, e, tb: errors.append(t.__name__)
try:
PRINT_REDIRECTOR.sigStdoutWrite.emit("stray output")
finally:
sys.excepthook = original
assert errors == [] # master: ['RuntimeError'] - LogViewer has been deletedMechanism: Where it stops being theoretical: adding pytest-qt tests that build |
Summary
PRINT_REDIRECTOR(a module-level singleton inmain_window.py) connected a lambda tosigStdoutWritefor eachMainWindow. The lambda was never disconnected, closed overself, and resolvedself.components["log"]late through the dictMainMixinholds as a class attribute — so it reached whicheverLogViewerregistered last. Once aLogViewerwas destroyed, the nextprint()called into a deleted C++ object and raisedRuntimeError: wrapped C/C++ object of type LogViewer has been deleted.Connecting the bound method
self.components["log"].appendinstead lets PyQt drop the connection automatically when theLogVieweris destroyed.Testing
Adds
test_print_redirector_released_with_window: destroys a window'sLogViewer, emits on the singleton, and asserts no exception reachessys.excepthook. It fails on the old lambda and passes with the fix.pytest tests/— full suite passes.The latent bug exists on master today; it just isn't exercised until multiple
MainWindows are created and torn down (as the display-modes tests do).Claude AI found and fixed the issue.