Uh oh!
There was an error while loading. Please reload this page.
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
10a2eb0Comparejmwright
commented
Jul 13, 2026
@markomarkovic What are the steps to reproduce this error? I am not seeing it, even in CI. |
markomarkovic
commented
Jul 17, 2026
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 ( importsysfromPyQt5importsipfromPyQt5.QtWidgetsimportQMessageBoxfromcq_editor.__main__importMainWindowfromcq_editor.main_windowimportPRINT_REDIRECTORdeftest_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 awayerrors= []
original, sys.excepthook=sys.excepthook, lambdat, e, tb: errors.append(t.__name__)
try:
PRINT_REDIRECTOR.sigStdoutWrite.emit("stray output")
finally:
sys.excepthook=originalasserterrors== [] # master: ['RuntimeError'] - LogViewer has been deletedMechanism: Where it stops being theoretical: adding pytest-qt tests that build |
jmwright
commented
Aug 19, 2026
@markomarkovic What was the reason for creating a new test file for main window tests instead of integrating this into the existing |
No real reason beside grouping. It's a lifetime regression test for the |
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.