Skip to content

Commit 3d297ff

Browse files
committed
Merge pull request #2558 from tacaswell/quiver_memory_leak
fixes issue #2556
2 parents 1ebe784 + f92ee59 commit 3d297ff

File tree

3 files changed

+115
-26
lines changed

3 files changed

+115
-26
lines changed

lib/matplotlib/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,7 @@ def tk_window_focus():
12571257
'matplotlib.tests.test_patheffects',
12581258
'matplotlib.tests.test_pickle',
12591259
'matplotlib.tests.test_png',
1260+
'matplotlib.tests.test_quiver',
12601261
'matplotlib.tests.test_rcparams',
12611262
'matplotlib.tests.test_scale',
12621263
'matplotlib.tests.test_simplification',

lib/matplotlib/quiver.py

+60-26
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
the Quiver code.
1515
"""
1616

17-
1817
from __future__ import print_function, division
18+
import weakref
19+
1920
import numpy as np
2021
from numpy import ma
21-
import matplotlib.collections as collections
22+
import matplotlib.collections as mcollections
2223
import matplotlib.transforms as transforms
2324
import matplotlib.text as mtext
2425
import matplotlib.artist as martist
@@ -219,9 +220,9 @@
219220

220221
class QuiverKey(martist.Artist):
221222
""" Labelled arrow for use as a quiver plot scale key."""
222-
halign = {'N': 'center', 'S': 'center', 'E': 'left', 'W': 'right'}
223-
valign = {'N': 'bottom', 'S': 'top', 'E': 'center', 'W': 'center'}
224-
pivot = {'N': 'mid', 'S': 'mid', 'E': 'tip', 'W': 'tail'}
223+
halign = {'N': 'center', 'S': 'center', 'E': 'left', 'W': 'right'}
224+
valign = {'N': 'bottom', 'S': 'top', 'E': 'center', 'W': 'center'}
225+
pivot = {'N': 'mid', 'S': 'mid', 'E': 'tip', 'W': 'tail'}
225226

226227
def __init__(self, Q, X, Y, U, label, **kw):
227228
martist.Artist.__init__(self)
@@ -235,13 +236,19 @@ def __init__(self, Q, X, Y, U, label, **kw):
235236
self._labelsep_inches = kw.pop('labelsep', 0.1)
236237
self.labelsep = (self._labelsep_inches * Q.ax.figure.dpi)
237238

239+
# try to prevent closure over the real self
240+
weak_self = weakref.ref(self)
241+
238242
def on_dpi_change(fig):
239-
self.labelsep = (self._labelsep_inches * fig.dpi)
240-
self._initialized = False # simple brute force update
241-
# works because _init is called
242-
# at the start of draw.
243+
self_weakref = weak_self()
244+
if self_weakref is not None:
245+
self_weakref.labelsep = (self_weakref._labelsep_inches*fig.dpi)
246+
self_weakref._initialized = False # simple brute force update
247+
# works because _init is called
248+
# at the start of draw.
243249

244-
Q.ax.figure.callbacks.connect('dpi_changed', on_dpi_change)
250+
self._cid = Q.ax.figure.callbacks.connect('dpi_changed',
251+
on_dpi_change)
245252

246253
self.labelpos = kw.pop('labelpos', 'N')
247254
self.labelcolor = kw.pop('labelcolor', None)
@@ -254,11 +261,21 @@ def on_dpi_change(fig):
254261
horizontalalignment=self.halign[self.labelpos],
255262
verticalalignment=self.valign[self.labelpos],
256263
fontproperties=font_manager.FontProperties(**_fp))
264+
257265
if self.labelcolor is not None:
258266
self.text.set_color(self.labelcolor)
259267
self._initialized = False
260268
self.zorder = Q.zorder + 0.1
261269

270+
def remove(self):
271+
"""
272+
Overload the remove method
273+
"""
274+
self.Q.ax.figure.callbacks.disconnect(self._cid)
275+
self._cid = None
276+
# pass the remove call up the stack
277+
martist.Artist.remove(self)
278+
262279
__init__.__doc__ = _quiverkey_doc
263280

264281
def _init(self):
@@ -275,7 +292,7 @@ def _init(self):
275292
self.Q.pivot = _pivot
276293
kw = self.Q.polykw
277294
kw.update(self.kw)
278-
self.vector = collections.PolyCollection(
295+
self.vector = mcollections.PolyCollection(
279296
self.verts,
280297
offsets=[(self.X, self.Y)],
281298
transOffset=self.get_transform(),
@@ -364,7 +381,7 @@ def _parse_args(*args):
364381
return X, Y, U, V, C
365382

366383

367-
class Quiver(collections.PolyCollection):
384+
class Quiver(mcollections.PolyCollection):
368385
"""
369386
Specialized PolyCollection for arrows.
370387
@@ -411,7 +428,7 @@ def __init__(self, ax, *args, **kw):
411428
self.transform = kw.pop('transform', ax.transData)
412429
kw.setdefault('facecolors', self.color)
413430
kw.setdefault('linewidths', (0,))
414-
collections.PolyCollection.__init__(self, [], offsets=self.XY,
431+
mcollections.PolyCollection.__init__(self, [], offsets=self.XY,
415432
transOffset=self.transform,
416433
closed=False,
417434
**kw)
@@ -422,14 +439,30 @@ def __init__(self, ax, *args, **kw):
422439
self.keyvec = None
423440
self.keytext = None
424441

425-
def on_dpi_change(fig):
426-
self._new_UV = True # vertices depend on width, span
427-
# which in turn depend on dpi
428-
self._initialized = False # simple brute force update
429-
# works because _init is called
430-
# at the start of draw.
442+
# try to prevent closure over the real self
443+
weak_self = weakref.ref(self)
431444

432-
self.ax.figure.callbacks.connect('dpi_changed', on_dpi_change)
445+
def on_dpi_change(fig):
446+
self_weakref = weak_self()
447+
if self_weakref is not None:
448+
self_weakref._new_UV = True # vertices depend on width, span
449+
# which in turn depend on dpi
450+
self_weakref._initialized = False # simple brute force update
451+
# works because _init is called
452+
# at the start of draw.
453+
454+
self._cid = self.ax.figure.callbacks.connect('dpi_changed',
455+
on_dpi_change)
456+
457+
def remove(self):
458+
"""
459+
Overload the remove method
460+
"""
461+
# disconnect the call back
462+
self.ax.figure.callbacks.disconnect(self._cid)
463+
self._cid = None
464+
# pass the remove call up the stack
465+
mcollections.PolyCollection.remove(self)
433466

434467
def _init(self):
435468
"""
@@ -456,7 +489,7 @@ def draw(self, renderer):
456489
verts = self._make_verts(self.U, self.V)
457490
self.set_verts(verts, closed=False)
458491
self._new_UV = False
459-
collections.PolyCollection.draw(self, renderer)
492+
mcollections.PolyCollection.draw(self, renderer)
460493

461494
def set_UVC(self, U, V, C=None):
462495
U = ma.masked_invalid(U, copy=False).ravel()
@@ -789,7 +822,7 @@ def _h_arrows(self, length):
789822
docstring.interpd.update(barbs_doc=_barbs_doc)
790823

791824

792-
class Barbs(collections.PolyCollection):
825+
class Barbs(mcollections.PolyCollection):
793826
'''
794827
Specialized PolyCollection for barbs.
795828
@@ -850,8 +883,9 @@ def __init__(self, ax, *args, **kw):
850883

851884
#Make a collection
852885
barb_size = self._length ** 2 / 4 # Empirically determined
853-
collections.PolyCollection.__init__(self, [], (barb_size,), offsets=xy,
854-
transOffset=transform, **kw)
886+
mcollections.PolyCollection.__init__(self, [], (barb_size,),
887+
offsets=xy,
888+
transOffset=transform, **kw)
855889
self.set_transform(transforms.IdentityTransform())
856890

857891
self.set_UVC(u, v, c)
@@ -1074,7 +1108,7 @@ def set_offsets(self, xy):
10741108
x, y, u, v = delete_masked_points(self.x.ravel(), self.y.ravel(),
10751109
self.u, self.v)
10761110
xy = np.hstack((x[:, np.newaxis], y[:, np.newaxis]))
1077-
collections.PolyCollection.set_offsets(self, xy)
1078-
set_offsets.__doc__ = collections.PolyCollection.set_offsets.__doc__
1111+
mcollections.PolyCollection.set_offsets(self, xy)
1112+
set_offsets.__doc__ = mcollections.PolyCollection.set_offsets.__doc__
10791113

10801114
barbs_doc = _barbs_doc

lib/matplotlib/tests/test_quiver.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import print_function
2+
import os
3+
import tempfile
4+
import numpy as np
5+
import sys
6+
from matplotlib import pyplot as plt
7+
from matplotlib.testing.decorators import cleanup
8+
9+
10+
WRITER_OUTPUT = dict(ffmpeg='mp4', ffmpeg_file='mp4',
11+
mencoder='mp4', mencoder_file='mp4',
12+
avconv='mp4', avconv_file='mp4',
13+
imagemagick='gif', imagemagick_file='gif')
14+
15+
16+
@cleanup
17+
def test_quiver_memory_leak():
18+
fig, ax = plt.subplots()
19+
20+
X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .04),
21+
np.arange(0, 2 * np.pi, .04))
22+
U = np.cos(X)
23+
V = np.sin(Y)
24+
25+
Q = ax.quiver(U, V)
26+
ttX = Q.X
27+
Q.remove()
28+
29+
del Q
30+
31+
assert sys.getrefcount(ttX) == 2
32+
33+
34+
@cleanup
35+
def test_quiver_key_memory_leak():
36+
fig, ax = plt.subplots()
37+
38+
X, Y = np.meshgrid(np.arange(0, 2 * np.pi, .04),
39+
np.arange(0, 2 * np.pi, .04))
40+
U = np.cos(X)
41+
V = np.sin(Y)
42+
43+
Q = ax.quiver(U, V)
44+
45+
qk = ax.quiverkey(Q, 0.5, 0.92, 2, r'$2 \frac{m}{s}$',
46+
labelpos='W',
47+
fontproperties={'weight': 'bold'})
48+
assert sys.getrefcount(qk) == 3
49+
qk.remove()
50+
assert sys.getrefcount(qk) == 2
51+
52+
if __name__ == '__main__':
53+
import nose
54+
nose.runmodule()

0 commit comments

Comments
 (0)