| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
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.
|
@markomarkovic What are the steps to reproduce this error? I am not seeing it, even in CI. |
Sorry, something went wrong.
|
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 (6daf9c0) — fails there, passes with this PR: 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: PRINT_REDIRECTOR is a module-level singleton that outlives any window. PyQt drops a connection to a QObject's bound method when that QObject is destroyed, but a lambda connection has no receiver to track, so it survives and calls into freed C++. Where it stops being theoretical: adding pytest-qt tests that build MainWindows. While writing tests for another feature, the full suite hit this repeatedly (once per previously-destroyed LogViewer) and pytest-qt escalated it into a SETUP ERROR on the following test. That's what prompted this fix. |
Sorry, something went wrong.
|
@markomarkovic What was the reason for creating a new test file for main window tests instead of integrating this into the existing test_app.py tests file? |
Sorry, something went wrong.
|
No real reason beside grouping. It's a lifetime regression test for the main_window.py singleton rather than app behavior test but there's nothing really stopping us from moving it into test_app.py if that's preferred. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Summary
PRINT_REDIRECTOR (a module-level singleton in main_window.py) connected a lambda to sigStdoutWrite for each MainWindow. The lambda was never disconnected, closed over self, and resolved self.components["log"] late through the dict 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: wrapped C/C++ object of type LogViewer has been deleted.
Connecting the bound method self.components["log"].append instead lets PyQt drop the connection automatically when the LogViewer is destroyed.
Testing
Adds test_print_redirector_released_with_window: destroys a window's LogViewer, emits on the singleton, and asserts no exception reaches sys.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.