diff --git a/CHANGELOG b/CHANGELOG index 934e6952..b9f51c84 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -84,6 +84,7 @@ pyqtgraph-0.9.9 [unreleased] - Fixed AxisItem.__init__(showValues=False) - Fixed TableWidget append / sort issues - Fixed AxisItem not resizing text area when setTicks() is used + - Removed a few cyclic references pyqtgraph-0.9.8 2013-11-24 diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 6f9b9112..d36282fa 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -2,6 +2,7 @@ from .Qt import QtCore from .ptime import time from . import ThreadsafeTimer +import weakref __all__ = ['SignalProxy'] @@ -34,7 +35,7 @@ class SignalProxy(QtCore.QObject): self.timer = ThreadsafeTimer.ThreadsafeTimer() self.timer.timeout.connect(self.flush) self.block = False - self.slot = slot + self.slot = weakref.ref(slot) self.lastFlushTime = None if slot is not None: self.sigDelayed.connect(slot) @@ -80,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot) + self.sigDelayed.disconnect(self.slot()) except: pass diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 71577422..6a915902 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -17,6 +17,7 @@ from .. import functions as fn import numpy as np from .. import debug as debug +import weakref __all__ = ['HistogramLUTItem'] @@ -42,7 +43,7 @@ class HistogramLUTItem(GraphicsWidget): """ GraphicsWidget.__init__(self) self.lut = None - self.imageItem = None + self.imageItem = lambda: None # fake a dead weakref self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -138,7 +139,7 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): - self.imageItem = img + self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result #self.gradientChanged() @@ -150,11 +151,11 @@ class HistogramLUTItem(GraphicsWidget): self.update() def gradientChanged(self): - if self.imageItem is not None: + if self.imageItem() is not None: if self.gradient.isLookupTrivial(): - self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8)) + self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8)) else: - self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result + self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None #if self.imageItem is not None: @@ -178,14 +179,14 @@ class HistogramLUTItem(GraphicsWidget): #self.update() def regionChanging(self): - if self.imageItem is not None: - self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): profiler = debug.Profiler() - h = self.imageItem.getHistogram() + h = self.imageItem().getHistogram() profiler('get histogram') if h[0] is None: return diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py new file mode 100644 index 00000000..0284852c --- /dev/null +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -0,0 +1,77 @@ +""" +Test for unwanted reference cycles + +""" +import pyqtgraph as pg +import numpy as np +import gc, weakref +app = pg.mkQApp() + +def assert_alldead(refs): + for ref in refs: + assert ref() is None + +def qObjectTree(root): + """Return root and its entire tree of qobject children""" + childs = [root] + for ch in pg.QtCore.QObject.children(root): + childs += qObjectTree(ch) + return childs + +def mkrefs(*objs): + """Return a list of weakrefs to each object in *objs. + QObject instances are expanded to include all child objects. + """ + allObjs = {} + for obj in objs: + if isinstance(obj, pg.QtCore.QObject): + obj = qObjectTree(obj) + else: + obj = [obj] + for o in obj: + allObjs[id(o)] = o + + return map(weakref.ref, allObjs.values()) + +def test_PlotWidget(): + def mkobjs(*args, **kwds): + w = pg.PlotWidget(*args, **kwds) + data = pg.np.array([1,5,2,4,3]) + c = w.plot(data, name='stuff') + w.addLegend() + + # test that connections do not keep objects alive + w.plotItem.vb.sigRangeChanged.connect(mkrefs) + app.focusChanged.connect(w.plotItem.vb.invertY) + + # return weakrefs to a bunch of objects that should die when the scope exits. + return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left')) + + for i in range(5): + assert_alldead(mkobjs()) + +def test_ImageView(): + def mkobjs(): + iv = pg.ImageView() + data = np.zeros((10,10,5)) + iv.setImage(data) + + return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) + + for i in range(5): + assert_alldead(mkobjs()) + +def test_GraphicsWindow(): + def mkobjs(): + w = pg.GraphicsWindow() + p1 = w.addPlot() + v1 = w.addViewBox() + return mkrefs(w, p1, v1) + + for i in range(5): + assert_alldead(mkobjs()) + + + +if __name__ == '__main__': + ot = test_PlotItem() diff --git a/pyqtgraph/tests/test_stability.py b/pyqtgraph/tests/test_stability.py new file mode 100644 index 00000000..a64e30e4 --- /dev/null +++ b/pyqtgraph/tests/test_stability.py @@ -0,0 +1,160 @@ +""" +PyQt/PySide stress test: + +Create lots of random widgets and graphics items, connect them together randomly, +the tear them down repeatedly. + +The purpose of this is to attempt to generate segmentation faults. +""" +from PyQt4.QtTest import QTest +import pyqtgraph as pg +from random import seed, randint +import sys, gc, weakref + +app = pg.mkQApp() + +seed(12345) + +widgetTypes = [ + pg.PlotWidget, + pg.ImageView, + pg.GraphicsView, + pg.QtGui.QWidget, + pg.QtGui.QTreeWidget, + pg.QtGui.QPushButton, + ] + +itemTypes = [ + pg.PlotCurveItem, + pg.ImageItem, + pg.PlotDataItem, + pg.ViewBox, + pg.QtGui.QGraphicsRectItem + ] + +widgets = [] +items = [] +allWidgets = weakref.WeakSet() + + +def crashtest(): + global allWidgets + try: + gc.disable() + actions = [ + createWidget, + #setParent, + forgetWidget, + showWidget, + processEvents, + #raiseException, + #addReference, + ] + + thread = WorkThread() + thread.start() + + while True: + try: + action = randItem(actions) + action() + print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets))) + except KeyboardInterrupt: + print("Caught interrupt; send another to exit.") + try: + for i in range(100): + QTest.qWait(100) + except KeyboardInterrupt: + thread.terminate() + break + except: + sys.excepthook(*sys.exc_info()) + finally: + gc.enable() + + + +class WorkThread(pg.QtCore.QThread): + '''Intended to give the gc an opportunity to run from a non-gui thread.''' + def run(self): + i = 0 + while True: + i += 1 + if (i % 1000000) == 0: + print('--worker--') + + +def randItem(items): + return items[randint(0, len(items)-1)] + +def p(msg): + print(msg) + sys.stdout.flush() + +def createWidget(): + p('create widget') + global widgets, allWidgets + if len(widgets) > 50: + return + widget = randItem(widgetTypes)() + widget.setWindowTitle(widget.__class__.__name__) + widgets.append(widget) + allWidgets.add(widget) + p(" %s" % widget) + return widget + +def setParent(): + p('set parent') + global widgets + if len(widgets) < 2: + return + child = parent = None + while child is parent: + child = randItem(widgets) + parent = randItem(widgets) + p(" %s parent of %s" % (parent, child)) + child.setParent(parent) + +def forgetWidget(): + p('forget widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widgets.remove(widget) + +def showWidget(): + p('show widget') + global widgets + if len(widgets) < 1: + return + widget = randItem(widgets) + p(' %s' % widget) + widget.show() + +def processEvents(): + p('process events') + QTest.qWait(25) + +class TstException(Exception): + pass + +def raiseException(): + p('raise exception') + raise TstException("A test exception") + +def addReference(): + p('add reference') + global widgets + if len(widgets) < 1: + return + obj1 = randItem(widgets) + obj2 = randItem(widgets) + p(' %s -> %s' % (obj1, obj2)) + obj1._testref = obj2 + + + +if __name__ == '__main__': + test_stability() \ No newline at end of file