From f6ded808efc89cb65d51edd2257c5a204b856317 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 26 Nov 2014 21:25:17 -0500 Subject: [PATCH] Fixed a few exit crashes, added unit tests to cover them --- pyqtgraph/GraphicsScene/GraphicsScene.py | 4 +-- pyqtgraph/__init__.py | 19 ++++++++++ pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +-- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 ++ pyqtgraph/tests/test_exit_crash.py | 38 ++++++++++++++++++++ pyqtgraph/widgets/GraphicsView.py | 11 ++++-- 7 files changed, 75 insertions(+), 9 deletions(-) create mode 100644 pyqtgraph/tests/test_exit_crash.py diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index c6afbe0f..6f5354dc 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -84,8 +84,8 @@ class GraphicsScene(QtGui.QGraphicsScene): cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - def __init__(self, clickRadius=2, moveDistance=5): - QtGui.QGraphicsScene.__init__(self) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): + QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) self.setMoveDistance(moveDistance) self.exportDirectory = None diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455..d539e06b 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -270,7 +270,12 @@ from .Qt import isQObjectAlive ## Attempts to work around exit crashes: import atexit +_cleanupCalled = False def cleanup(): + global _cleanupCalled + if _cleanupCalled: + return + if not getConfigOption('exitCleanup'): return @@ -295,8 +300,22 @@ def cleanup(): s.addItem(o) except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object continue + _cleanupCalled = True + atexit.register(cleanup) +# Call cleanup when QApplication quits. This is necessary because sometimes +# the QApplication will quit before the atexit callbacks are invoked. +# Note: cannot connect this function until QApplication has been created, so +# instead we have GraphicsView.__init__ call this for us. +_cleanupConnected = False +def _connectCleanup(): + global _cleanupConnected + if _cleanupConnected: + return + QtGui.QApplication.instance().aboutToQuit.connect(cleanup) + _cleanupConnected = True + ## Optional function for exiting immediately (with some manual teardown) def exit(): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6a915902..89ebef3e 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -49,7 +49,7 @@ class HistogramLUTItem(GraphicsWidget): self.setLayout(self.layout) self.layout.setContentsMargins(1,1,1,1) self.layout.setSpacing(0) - self.vb = ViewBox() + self.vb = ViewBox(parent=self) self.vb.setMaximumWidth(152) self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=False, y=True) @@ -59,7 +59,7 @@ class HistogramLUTItem(GraphicsWidget): self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.region.setZValue(1000) self.vb.addItem(self.region) - self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10) + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.gradient, 0, 2) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f8959e22..4f10b0e3 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -145,7 +145,7 @@ class PlotItem(GraphicsWidget): self.layout.setVerticalSpacing(0) if viewBox is None: - viewBox = ViewBox() + viewBox = ViewBox(parent=self) self.vb = viewBox self.vb.sigStateChanged.connect(self.viewStateChanged) self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus @@ -168,14 +168,14 @@ class PlotItem(GraphicsWidget): axisItems = {} self.axes = {} for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, AxisItem(orientation=k)) + axis = axisItems.get(k, AxisItem(orientation=k, parent=self)) axis.linkToView(self.vb) self.axes[k] = {'item': axis, 'pos': pos} self.layout.addItem(axis, *pos) axis.setZValue(-1000) axis.setFlag(axis.ItemNegativeZStacksBehindParent) - self.titleLabel = LabelItem('', size='11pt') + self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index ec9c20fe..900c2038 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1696,6 +1696,8 @@ class ViewBox(GraphicsWidget): def forgetView(vid, name): if ViewBox is None: ## can happen as python is shutting down return + if QtGui.QApplication.instance() is None: + return ## Called with ID and name of view (the view itself is no longer available) for v in list(ViewBox.AllViews.keys()): if id(v) == vid: diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py new file mode 100644 index 00000000..69181f21 --- /dev/null +++ b/pyqtgraph/tests/test_exit_crash.py @@ -0,0 +1,38 @@ +import os, sys, subprocess, tempfile +import pyqtgraph as pg + + +code = """ +import sys +sys.path.insert(0, '{path}') +import pyqtgraph as pg +app = pg.mkQApp() +w = pg.{classname}({args}) +""" + + +def test_exit_crash(): + # For each Widget subclass, run a simple python script that creates an + # instance and then shuts down. The intent is to check for segmentation + # faults when each script exits. + tmp = tempfile.mktemp(".py") + path = os.path.dirname(pg.__file__) + + initArgs = { + 'CheckTable': "[]", + 'ProgressDialog': '"msg"', + 'VerticalLabel': '"msg"', + } + + for name in dir(pg): + obj = getattr(pg, name) + if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget): + continue + + print name + argstr = initArgs.get(name, "") + open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) + proc = subprocess.Popen([sys.executable, tmp]) + assert proc.wait() == 0 + + os.remove(tmp) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 3273ac60..4062be94 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -71,6 +71,13 @@ class GraphicsView(QtGui.QGraphicsView): QtGui.QGraphicsView.__init__(self, parent) + # This connects a cleanup function to QApplication.aboutToQuit. It is + # called from here because we have no good way to react when the + # QApplication is created by the user. + # See pyqtgraph.__init__.py + from .. import _connectCleanup + _connectCleanup() + if useOpenGL is None: useOpenGL = getConfigOption('useOpenGL') @@ -102,7 +109,8 @@ class GraphicsView(QtGui.QGraphicsView): self.currentItem = None self.clearMouse() self.updateMatrix() - self.sceneObj = GraphicsScene() + # GraphicsScene must have parent or expect crashes! + self.sceneObj = GraphicsScene(parent=self) self.setScene(self.sceneObj) ## Workaround for PySide crash @@ -143,7 +151,6 @@ class GraphicsView(QtGui.QGraphicsView): def paintEvent(self, ev): self.scene().prepareForPaint() - #print "GV: paint", ev.rect() return QtGui.QGraphicsView.paintEvent(self, ev) def render(self, *args, **kwds):