Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions cq_editor/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,9 +291,11 @@ def prepare_panes(self):
for d in self.docks.values():
d.show()

PRINT_REDIRECTOR.sigStdoutWrite.connect(
lambda text: self.components["log"].append(text)
)
# Connect the bound method rather than a lambda: PRINT_REDIRECTOR is a
# module-level singleton, and PyQt drops a connection to a QObject's
# bound method when that QObject is destroyed. A lambda would outlive
# the LogViewer and call into a deleted C++ object.
PRINT_REDIRECTOR.sigStdoutWrite.connect(self.components["log"].append)

def prepare_menubar(self):

Expand Down
37 changes: 37 additions & 0 deletions tests/test_main_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sys

from PyQt5.QtWidgets import QMessageBox
from PyQt5 import sip

from cq_editor.__main__ import MainWindow
from cq_editor.main_window import PRINT_REDIRECTOR


def test_print_redirector_released_with_window(qtbot, mocker):
"""
PRINT_REDIRECTOR is a module-level singleton that outlives any MainWindow.
Once a window's LogViewer is destroyed, a write must be dropped rather than
delivered to the now-deleted C++ object (which raises "wrapped C/C++ object
of type LogViewer has been deleted").
"""

mocker.patch.object(QMessageBox, "question", return_value=QMessageBox.Yes)
mocker.patch.object(QMessageBox, "warning", return_value=QMessageBox.Discard)

win = MainWindow()
qtbot.addWidget(win)

# destroy this window's LogViewer, as tearing the window down would
sip.delete(win.components["log"])

# PyQt routes an exception raised inside a slot to sys.excepthook rather
# than propagating it, so capture it there while emitting
errors = []
original_hook = sys.excepthook
sys.excepthook = lambda exc_type, *rest: errors.append(exc_type)
try:
PRINT_REDIRECTOR.sigStdoutWrite.emit("stray output after close")
finally:
sys.excepthook = original_hook

assert errors == []