Skip to content

Commit 255b240

Browse files
committed
Merge branch 'stability_tests' into develop
2 parents 5a8d77d + 24c25b6 commit 255b240

File tree

5 files changed

+250
-10
lines changed

5 files changed

+250
-10
lines changed

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ pyqtgraph-0.9.9 [unreleased]
8484
- Fixed AxisItem.__init__(showValues=False)
8585
- Fixed TableWidget append / sort issues
8686
- Fixed AxisItem not resizing text area when setTicks() is used
87+
- Removed a few cyclic references
8788

8889
pyqtgraph-0.9.8 2013-11-24
8990

pyqtgraph/SignalProxy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .Qt import QtCore
33
from .ptime import time
44
from . import ThreadsafeTimer
5+
import weakref
56

67
__all__ = ['SignalProxy']
78

@@ -34,7 +35,7 @@ def __init__(self, signal, delay=0.3, rateLimit=0, slot=None):
3435
self.timer = ThreadsafeTimer.ThreadsafeTimer()
3536
self.timer.timeout.connect(self.flush)
3637
self.block = False
37-
self.slot = slot
38+
self.slot = weakref.ref(slot)
3839
self.lastFlushTime = None
3940
if slot is not None:
4041
self.sigDelayed.connect(slot)
@@ -80,7 +81,7 @@ def disconnect(self):
8081
except:
8182
pass
8283
try:
83-
self.sigDelayed.disconnect(self.slot)
84+
self.sigDelayed.disconnect(self.slot())
8485
except:
8586
pass
8687

pyqtgraph/graphicsItems/HistogramLUTItem.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import numpy as np
1818
from .. import debug as debug
1919

20+
import weakref
2021

2122
__all__ = ['HistogramLUTItem']
2223

@@ -42,7 +43,7 @@ def __init__(self, image=None, fillHistogram=True):
4243
"""
4344
GraphicsWidget.__init__(self)
4445
self.lut = None
45-
self.imageItem = None
46+
self.imageItem = lambda: None # fake a dead weakref
4647

4748
self.layout = QtGui.QGraphicsGridLayout()
4849
self.setLayout(self.layout)
@@ -138,7 +139,7 @@ def autoHistogramRange(self):
138139
#self.region.setBounds([vr.top(), vr.bottom()])
139140

140141
def setImageItem(self, img):
141-
self.imageItem = img
142+
self.imageItem = weakref.ref(img)
142143
img.sigImageChanged.connect(self.imageChanged)
143144
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
144145
#self.gradientChanged()
@@ -150,11 +151,11 @@ def viewRangeChanged(self):
150151
self.update()
151152

152153
def gradientChanged(self):
153-
if self.imageItem is not None:
154+
if self.imageItem() is not None:
154155
if self.gradient.isLookupTrivial():
155-
self.imageItem.setLookupTable(None) #lambda x: x.astype(np.uint8))
156+
self.imageItem().setLookupTable(None) #lambda x: x.astype(np.uint8))
156157
else:
157-
self.imageItem.setLookupTable(self.getLookupTable) ## send function pointer, not the result
158+
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
158159

159160
self.lut = None
160161
#if self.imageItem is not None:
@@ -178,14 +179,14 @@ def regionChanged(self):
178179
#self.update()
179180

180181
def regionChanging(self):
181-
if self.imageItem is not None:
182-
self.imageItem.setLevels(self.region.getRegion())
182+
if self.imageItem() is not None:
183+
self.imageItem().setLevels(self.region.getRegion())
183184
self.sigLevelsChanged.emit(self)
184185
self.update()
185186

186187
def imageChanged(self, autoLevel=False, autoRange=False):
187188
profiler = debug.Profiler()
188-
h = self.imageItem.getHistogram()
189+
h = self.imageItem().getHistogram()
189190
profiler('get histogram')
190191
if h[0] is None:
191192
return

pyqtgraph/tests/test_ref_cycles.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""
2+
Test for unwanted reference cycles
3+
4+
"""
5+
import pyqtgraph as pg
6+
import numpy as np
7+
import gc, weakref
8+
app = pg.mkQApp()
9+
10+
def assert_alldead(refs):
11+
for ref in refs:
12+
assert ref() is None
13+
14+
def qObjectTree(root):
15+
"""Return root and its entire tree of qobject children"""
16+
childs = [root]
17+
for ch in pg.QtCore.QObject.children(root):
18+
childs += qObjectTree(ch)
19+
return childs
20+
21+
def mkrefs(*objs):
22+
"""Return a list of weakrefs to each object in *objs.
23+
QObject instances are expanded to include all child objects.
24+
"""
25+
allObjs = {}
26+
for obj in objs:
27+
if isinstance(obj, pg.QtCore.QObject):
28+
obj = qObjectTree(obj)
29+
else:
30+
obj = [obj]
31+
for o in obj:
32+
allObjs[id(o)] = o
33+
34+
return map(weakref.ref, allObjs.values())
35+
36+
def test_PlotWidget():
37+
def mkobjs(*args, **kwds):
38+
w = pg.PlotWidget(*args, **kwds)
39+
data = pg.np.array([1,5,2,4,3])
40+
c = w.plot(data, name='stuff')
41+
w.addLegend()
42+
43+
# test that connections do not keep objects alive
44+
w.plotItem.vb.sigRangeChanged.connect(mkrefs)
45+
app.focusChanged.connect(w.plotItem.vb.invertY)
46+
47+
# return weakrefs to a bunch of objects that should die when the scope exits.
48+
return mkrefs(w, c, data, w.plotItem, w.plotItem.vb, w.plotItem.getMenu(), w.plotItem.getAxis('left'))
49+
50+
for i in range(5):
51+
assert_alldead(mkobjs())
52+
53+
def test_ImageView():
54+
def mkobjs():
55+
iv = pg.ImageView()
56+
data = np.zeros((10,10,5))
57+
iv.setImage(data)
58+
59+
return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data)
60+
61+
for i in range(5):
62+
assert_alldead(mkobjs())
63+
64+
def test_GraphicsWindow():
65+
def mkobjs():
66+
w = pg.GraphicsWindow()
67+
p1 = w.addPlot()
68+
v1 = w.addViewBox()
69+
return mkrefs(w, p1, v1)
70+
71+
for i in range(5):
72+
assert_alldead(mkobjs())
73+
74+
75+
76+
if __name__ == '__main__':
77+
ot = test_PlotItem()

pyqtgraph/tests/test_stability.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""
2+
PyQt/PySide stress test:
3+
4+
Create lots of random widgets and graphics items, connect them together randomly,
5+
the tear them down repeatedly.
6+
7+
The purpose of this is to attempt to generate segmentation faults.
8+
"""
9+
from PyQt4.QtTest import QTest
10+
import pyqtgraph as pg
11+
from random import seed, randint
12+
import sys, gc, weakref
13+
14+
app = pg.mkQApp()
15+
16+
seed(12345)
17+
18+
widgetTypes = [
19+
pg.PlotWidget,
20+
pg.ImageView,
21+
pg.GraphicsView,
22+
pg.QtGui.QWidget,
23+
pg.QtGui.QTreeWidget,
24+
pg.QtGui.QPushButton,
25+
]
26+
27+
itemTypes = [
28+
pg.PlotCurveItem,
29+
pg.ImageItem,
30+
pg.PlotDataItem,
31+
pg.ViewBox,
32+
pg.QtGui.QGraphicsRectItem
33+
]
34+
35+
widgets = []
36+
items = []
37+
allWidgets = weakref.WeakSet()
38+
39+
40+
def crashtest():
41+
global allWidgets
42+
try:
43+
gc.disable()
44+
actions = [
45+
createWidget,
46+
#setParent,
47+
forgetWidget,
48+
showWidget,
49+
processEvents,
50+
#raiseException,
51+
#addReference,
52+
]
53+
54+
thread = WorkThread()
55+
thread.start()
56+
57+
while True:
58+
try:
59+
action = randItem(actions)
60+
action()
61+
print('[%d widgets alive, %d zombie]' % (len(allWidgets), len(allWidgets) - len(widgets)))
62+
except KeyboardInterrupt:
63+
print("Caught interrupt; send another to exit.")
64+
try:
65+
for i in range(100):
66+
QTest.qWait(100)
67+
except KeyboardInterrupt:
68+
thread.terminate()
69+
break
70+
except:
71+
sys.excepthook(*sys.exc_info())
72+
finally:
73+
gc.enable()
74+
75+
76+
77+
class WorkThread(pg.QtCore.QThread):
78+
'''Intended to give the gc an opportunity to run from a non-gui thread.'''
79+
def run(self):
80+
i = 0
81+
while True:
82+
i += 1
83+
if (i % 1000000) == 0:
84+
print('--worker--')
85+
86+
87+
def randItem(items):
88+
return items[randint(0, len(items)-1)]
89+
90+
def p(msg):
91+
print(msg)
92+
sys.stdout.flush()
93+
94+
def createWidget():
95+
p('create widget')
96+
global widgets, allWidgets
97+
if len(widgets) > 50:
98+
return
99+
widget = randItem(widgetTypes)()
100+
widget.setWindowTitle(widget.__class__.__name__)
101+
widgets.append(widget)
102+
allWidgets.add(widget)
103+
p(" %s" % widget)
104+
return widget
105+
106+
def setParent():
107+
p('set parent')
108+
global widgets
109+
if len(widgets) < 2:
110+
return
111+
child = parent = None
112+
while child is parent:
113+
child = randItem(widgets)
114+
parent = randItem(widgets)
115+
p(" %s parent of %s" % (parent, child))
116+
child.setParent(parent)
117+
118+
def forgetWidget():
119+
p('forget widget')
120+
global widgets
121+
if len(widgets) < 1:
122+
return
123+
widget = randItem(widgets)
124+
p(' %s' % widget)
125+
widgets.remove(widget)
126+
127+
def showWidget():
128+
p('show widget')
129+
global widgets
130+
if len(widgets) < 1:
131+
return
132+
widget = randItem(widgets)
133+
p(' %s' % widget)
134+
widget.show()
135+
136+
def processEvents():
137+
p('process events')
138+
QTest.qWait(25)
139+
140+
class TstException(Exception):
141+
pass
142+
143+
def raiseException():
144+
p('raise exception')
145+
raise TstException("A test exception")
146+
147+
def addReference():
148+
p('add reference')
149+
global widgets
150+
if len(widgets) < 1:
151+
return
152+
obj1 = randItem(widgets)
153+
obj2 = randItem(widgets)
154+
p(' %s -> %s' % (obj1, obj2))
155+
obj1._testref = obj2
156+
157+
158+
159+
if __name__ == '__main__':
160+
test_stability()

0 commit comments

Comments
 (0)