Skip to content

Commit 7595101

Browse files
committed
Fix PySide6 compatibility issues causing segfaults in test suite
- Use `object` instead of C++ type strings in Signal declarations (PySide6 segfaults with "QMouseEvent", "QEvent", "QPointF" strings) - Check QObject validity via `is_qobject_valid()` before accessing widgets in __del__, closeEvent, and panel close operations - Restructure BaseSyncPlot.__init__ to defer widget operations until after Qt __init__ completes (PySide6 requirement) - Replace deprecated `exec_()` calls with `exec()`
1 parent cfcc470 commit 7595101

File tree

7 files changed

+49
-33
lines changed

7 files changed

+49
-33
lines changed

plotpy/events.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -727,7 +727,7 @@ class ClickHandler(QC.QObject):
727727
"""
728728

729729
#: Signal emitted by ClickHandler on mouse click
730-
SIG_CLICK_EVENT = QC.Signal(object, "QEvent")
730+
SIG_CLICK_EVENT = QC.Signal(object, object)
731731

732732
def __init__(
733733
self,
@@ -1134,16 +1134,16 @@ class QtDragHandler(DragHandler):
11341134
"""Class to handle drag events using Qt signals."""
11351135

11361136
#: Signal emitted by QtDragHandler when starting tracking
1137-
SIG_START_TRACKING = QC.Signal(object, "QMouseEvent")
1137+
SIG_START_TRACKING = QC.Signal(object, object)
11381138

11391139
#: Signal emitted by QtDragHandler when stopping tracking and not moving
1140-
SIG_STOP_NOT_MOVING = QC.Signal(object, "QMouseEvent")
1140+
SIG_STOP_NOT_MOVING = QC.Signal(object, object)
11411141

11421142
#: Signal emitted by QtDragHandler when stopping tracking and moving
1143-
SIG_STOP_MOVING = QC.Signal(object, "QMouseEvent")
1143+
SIG_STOP_MOVING = QC.Signal(object, object)
11441144

11451145
#: Signal emitted by QtDragHandler when moving
1146-
SIG_MOVE = QC.Signal(object, "QMouseEvent")
1146+
SIG_MOVE = QC.Signal(object, object)
11471147

11481148
def start_tracking(self, filter: StatefulEventFilter, event: QMouseEvent) -> None:
11491149
"""Starts tracking the drag event.
@@ -1636,7 +1636,7 @@ class RectangularSelectionHandler(DragHandler):
16361636
"""
16371637

16381638
#: Signal emitted by RectangularSelectionHandler when ending selection
1639-
SIG_END_RECT = QC.Signal(object, "QPointF", "QPointF")
1639+
SIG_END_RECT = QC.Signal(object, object, object)
16401640

16411641
def __init__(
16421642
self,

plotpy/plot/base.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import numpy as np
2828
import qwt
2929
from guidata.configtools import get_font
30+
from guidata.qthelpers import is_qobject_valid
3031
from qtpy import QtCore as QC
3132
from qtpy import QtGui as QG
3233
from qtpy import QtWidgets as QW
@@ -447,18 +448,16 @@ def __del__(self):
447448
# Sometimes, an obscure exception happens when we quit an application
448449
# because if we don't remove the eventFilter it can still be called
449450
# after the filter object has been destroyed by Python.
451+
# Note: PySide6 segfaults instead of raising RuntimeError when accessing
452+
# a deleted C++ object, so we must check validity before calling methods.
453+
if not is_qobject_valid(self):
454+
return
450455
canvas: qwt.QwtPlotCanvas = self.canvas()
451-
if canvas:
456+
if canvas and is_qobject_valid(canvas) and is_qobject_valid(self.filter):
452457
try:
453458
canvas.removeEventFilter(self.filter)
454-
except RuntimeError as exc:
455-
# Depending on which widget owns the plot,
456-
# Qt may have already deleted the canvas when
457-
# the plot is deleted.
458-
if "C++ object" not in str(exc):
459-
raise
460-
except ValueError:
461-
# This happens when object has already been deleted
459+
except (RuntimeError, ValueError):
460+
# Widget/filter may have already been deleted
462461
pass
463462

464463
def update_color_mode(self) -> None:

plotpy/plot/interactive.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import numpy as np
2020
from guidata.configtools import get_icon
2121
from guidata.env import execenv
22-
from guidata.qthelpers import win32_fix_title_bar_background
22+
from guidata.qthelpers import is_qobject_valid, win32_fix_title_bar_background
2323
from qtpy import QtCore as QC
2424
from qtpy import QtGui as QG
2525
from qtpy import QtWidgets as QW
@@ -94,10 +94,9 @@ def closeEvent(self, event):
9494
if _figures.pop(figure_title) == _current_fig:
9595
_current_fig = None
9696
_current_axes = None
97-
self.itemlist.close()
98-
self.contrast.close()
99-
self.xcsw.close()
100-
self.ycsw.close()
97+
for panel in (self.itemlist, self.contrast, self.xcsw, self.ycsw):
98+
if is_qobject_valid(panel):
99+
panel.close()
101100
event.accept()
102101

103102
def add_plot(self, i, j, plot):

plotpy/plot/plotwidget.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import TYPE_CHECKING, Any
99

1010
from guidata.configtools import get_icon
11-
from guidata.qthelpers import win32_fix_title_bar_background
11+
from guidata.qthelpers import is_qobject_valid, win32_fix_title_bar_background
1212
from qtpy import QtCore as QC
1313
from qtpy import QtGui as QG
1414
from qtpy import QtWidgets as QW
@@ -827,7 +827,9 @@ def closeEvent(self, event) -> None:
827827
# parent widget: otherwise, this panel will stay open after the main
828828
# window has been closed which is not the expected behavior)
829829
for panel in self.manager.panels:
830-
self.manager.get_panel(panel).close()
830+
panel_widget = self.manager.get_panel(panel)
831+
if is_qobject_valid(panel_widget):
832+
panel_widget.close()
831833
QW.QMainWindow.closeEvent(self, event)
832834

833835

@@ -903,16 +905,30 @@ def __init__(
903905
) -> None:
904906
self.manager = PlotManager(None)
905907
self.manager.set_main(self)
906-
self.subplotwidget = SubplotWidget(self.manager, parent=self, options=options)
908+
# Note: parent is not set here and widget operations on `self` are
909+
# deferred to _finalize_init() because PySide6 requires the subclass's
910+
# __init__ to have fully completed before widget methods can be called.
911+
self.subplotwidget = SubplotWidget(self.manager, parent=None, options=options)
912+
self._toolbar_visible = toolbar
913+
self.auto_tools = auto_tools
914+
self._rescale_timer: QC.QTimer | None = None
915+
self._init_title = title
916+
self._init_icon = icon
917+
self._init_size = size
918+
919+
def _finalize_init(self) -> None:
920+
"""Finalize initialization after Qt widget __init__ has completed.
921+
922+
This is called by subclasses in their __init__ after calling both
923+
QMainWindow/QDialog.__init__ and BaseSyncPlot.__init__.
924+
"""
907925
self.toolbar = QW.QToolBar(_("Tools"), self)
908-
self.toolbar.setVisible(toolbar)
926+
self.toolbar.setVisible(self._toolbar_visible)
909927
self.manager.add_toolbar(self.toolbar, "default")
910928
self.toolbar.setMovable(True)
911929
self.toolbar.setFloatable(True)
912-
self.auto_tools = auto_tools
913-
self._rescale_timer: QC.QTimer | None = None
914-
set_widget_title_icon(self, title, icon, size)
915-
# Note: setup_layout() is called by subclasses after Qt widget initialization
930+
set_widget_title_icon(self, self._init_title, self._init_icon, self._init_size)
931+
# Note: setup_layout() is called by subclasses after _finalize_init()
916932

917933
def setup_layout(self) -> None:
918934
"""Setup the layout - to be implemented by subclasses"""
@@ -1048,8 +1064,9 @@ def __init__(
10481064
) -> None:
10491065
self.subplotwidget: SubplotWidget
10501066
self.toolbar: QW.QToolBar
1051-
QW.QMainWindow.__init__(self, parent)
10521067
BaseSyncPlot.__init__(self, toolbar, options, auto_tools, title, icon, size)
1068+
QW.QMainWindow.__init__(self, parent)
1069+
self._finalize_init()
10531070
self.setup_layout()
10541071

10551072
def showEvent(self, event): # pylint: disable=C0103
@@ -1107,8 +1124,9 @@ def __init__(
11071124
) -> None:
11081125
self.subplotwidget: SubplotWidget
11091126
self.toolbar: QW.QToolBar
1110-
QW.QDialog.__init__(self, parent)
11111127
BaseSyncPlot.__init__(self, toolbar, options, auto_tools, title, icon, size)
1128+
QW.QDialog.__init__(self, parent)
1129+
self._finalize_init()
11121130
self.setup_layout()
11131131
self.setWindowFlags(QC.Qt.Window)
11141132

plotpy/tests/widgets/test_plot_timecurve.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,4 +416,4 @@ def update_all_curves(self, force=True):
416416
# win.showMaximized()
417417
win.resize(800, 400)
418418
win.show()
419-
app.exec_()
419+
app.exec()

plotpy/tools/curve.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1266,7 +1266,7 @@ def edit_curve_data(item: CurveItem) -> None:
12661266

12671267
dialog = ArrayEditor(item.plot())
12681268
dialog.setup_and_check(data)
1269-
if dialog.exec_():
1269+
if dialog.exec():
12701270
if data.shape[1] > 2:
12711271
if data.shape[1] == 3:
12721272
x, y, tmp = data.T

plotpy/tools/image.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1372,4 +1372,4 @@ def edit_image_data(item) -> None:
13721372
"""Edit image item data in array editor"""
13731373
dialog = ArrayEditor(item.plot())
13741374
dialog.setup_and_check(item.data)
1375-
dialog.exec_()
1375+
dialog.exec()

0 commit comments

Comments
 (0)