Skip to content

Commit 04ebedc

Browse files
committed
Require calling a _BoundMethodProxy to get the underlying callable.
Code that used to call `_proxy(...)` now needs to call `_proxy()(...)`. Instead of catching a ReferenceError, that could be either raised by a failure to dereference the proxy, or by the underlying callable, one can now check that `_proxy()` does not return None (to distinguish between the two cases). This is the same design as the stdlib's WeakMethod.
1 parent fb17040 commit 04ebedc

File tree

2 files changed

+57
-127
lines changed

2 files changed

+57
-127
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
`CallbackRegistry` now stores callbacks using stdlib's `WeakMethods`
2+
````````````````````````````````````````````````````````````````````
3+
4+
In particular, this implies that ``CallbackRegistry.callbacks[signal]`` is now
5+
a mapping of callback ids to `WeakMethods` (i.e., they need to be first called
6+
with no arguments to retrieve the method itself).

lib/matplotlib/cbook/__init__.py

+51-127
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import glob
1515
import gzip
1616
import io
17-
from itertools import repeat
17+
import inspect
18+
import itertools
1819
import locale
1920
import numbers
2021
import operator
@@ -26,7 +27,8 @@
2627
import traceback
2728
import types
2829
import warnings
29-
from weakref import ref, WeakKeyDictionary
30+
import weakref
31+
from weakref import WeakMethod
3032

3133
import numpy as np
3234

@@ -61,100 +63,26 @@ def unicode_safe(s):
6163
return s
6264

6365

64-
class _BoundMethodProxy(object):
65-
"""
66-
Our own proxy object which enables weak references to bound and unbound
67-
methods and arbitrary callables. Pulls information about the function,
68-
class, and instance out of a bound method. Stores a weak reference to the
69-
instance to support garbage collection.
66+
def _exception_printer(exc):
67+
traceback.print_exc()
7068

71-
@organization: IBM Corporation
72-
@copyright: Copyright (c) 2005, 2006 IBM Corporation
73-
@license: The BSD License
7469

75-
Minor bugfixes by Michael Droettboom
70+
class _StrongRef:
71+
"""
72+
Wrapper similar to a weakref, but keeping a strong reference to the object.
7673
"""
77-
def __init__(self, cb):
78-
self._hash = hash(cb)
79-
self._destroy_callbacks = []
80-
try:
81-
try:
82-
self.inst = ref(cb.__self__, self._destroy)
83-
except TypeError:
84-
self.inst = None
85-
self.func = cb.__func__
86-
self.klass = cb.__self__.__class__
87-
except AttributeError:
88-
self.inst = None
89-
self.func = cb
90-
self.klass = None
91-
92-
def add_destroy_callback(self, callback):
93-
self._destroy_callbacks.append(_BoundMethodProxy(callback))
94-
95-
def _destroy(self, wk):
96-
for callback in self._destroy_callbacks:
97-
try:
98-
callback(self)
99-
except ReferenceError:
100-
pass
101-
102-
def __getstate__(self):
103-
d = self.__dict__.copy()
104-
# de-weak reference inst
105-
inst = d['inst']
106-
if inst is not None:
107-
d['inst'] = inst()
108-
return d
109-
110-
def __setstate__(self, statedict):
111-
self.__dict__ = statedict
112-
inst = statedict['inst']
113-
# turn inst back into a weakref
114-
if inst is not None:
115-
self.inst = ref(inst)
116-
117-
def __call__(self, *args, **kwargs):
118-
"""
119-
Proxy for a call to the weak referenced object. Take
120-
arbitrary params to pass to the callable.
121-
122-
Raises `ReferenceError`: When the weak reference refers to
123-
a dead object
124-
"""
125-
if self.inst is not None and self.inst() is None:
126-
raise ReferenceError
127-
elif self.inst is not None:
128-
# build a new instance method with a strong reference to the
129-
# instance
13074

131-
mtd = types.MethodType(self.func, self.inst())
75+
def __init__(self, obj):
76+
self._obj = obj
13277

133-
else:
134-
# not a bound method, just return the func
135-
mtd = self.func
136-
# invoke the callable and return the result
137-
return mtd(*args, **kwargs)
78+
def __call__(self):
79+
return self._obj
13880

13981
def __eq__(self, other):
140-
"""
141-
Compare the held function and instance with that held by
142-
another proxy.
143-
"""
144-
try:
145-
if self.inst is None:
146-
return self.func == other.func and other.inst is None
147-
else:
148-
return self.func == other.func and self.inst() == other.inst()
149-
except Exception:
150-
return False
82+
return isinstance(other, _StrongRef) and self._obj == other._obj
15183

15284
def __hash__(self):
153-
return self._hash
154-
155-
156-
def _exception_printer(exc):
157-
traceback.print_exc()
85+
return hash(self._obj)
15886

15987

16088
class CallbackRegistry(object):
@@ -179,20 +107,13 @@ class CallbackRegistry(object):
179107
>>> callbacks.disconnect(id_eat)
180108
>>> callbacks.process('eat', 456) # nothing will be called
181109
182-
In practice, one should always disconnect all callbacks when they
183-
are no longer needed to avoid dangling references (and thus memory
184-
leaks). However, real code in matplotlib rarely does so, and due
185-
to its design, it is rather difficult to place this kind of code.
186-
To get around this, and prevent this class of memory leaks, we
187-
instead store weak references to bound methods only, so when the
188-
destination object needs to die, the CallbackRegistry won't keep
189-
it alive. The Python stdlib weakref module can not create weak
190-
references to bound methods directly, so we need to create a proxy
191-
object to handle weak references to bound methods (or regular free
192-
functions). This technique was shared by Peter Parente on his
193-
`"Mindtrove" blog
194-
<http://mindtrove.info/python-weak-references/>`_.
195-
110+
In practice, one should always disconnect all callbacks when they are
111+
no longer needed to avoid dangling references (and thus memory leaks).
112+
However, real code in Matplotlib rarely does so, and due to its design,
113+
it is rather difficult to place this kind of code. To get around this,
114+
and prevent this class of memory leaks, we instead store weak references
115+
to bound methods only, so when the destination object needs to die, the
116+
CallbackRegistry won't keep it alive.
196117
197118
Parameters
198119
----------
@@ -211,12 +132,17 @@ def handler(exc: Exception) -> None:
211132
212133
def h(exc):
213134
traceback.print_exc()
214-
215135
"""
136+
137+
# We maintain two mappings:
138+
# callbacks: signal -> {cid -> callback}
139+
# _func_cid_map: signal -> {callback -> cid}
140+
# (actually, callbacks are weakrefs to the actual callbacks).
141+
216142
def __init__(self, exception_handler=_exception_printer):
217143
self.exception_handler = exception_handler
218-
self.callbacks = dict()
219-
self._cid = 0
144+
self.callbacks = {}
145+
self._cid_gen = itertools.count()
220146
self._func_cid_map = {}
221147

222148
# In general, callbacks may not be pickled; thus, we simply recreate an
@@ -236,18 +162,17 @@ def __setstate__(self, state):
236162
def connect(self, s, func):
237163
"""Register *func* to be called when signal *s* is generated.
238164
"""
239-
self._func_cid_map.setdefault(s, WeakKeyDictionary())
240-
# Note proxy not needed in python 3.
241-
# TODO rewrite this when support for python2.x gets dropped.
242-
proxy = _BoundMethodProxy(func)
165+
self._func_cid_map.setdefault(s, {})
166+
try:
167+
proxy = WeakMethod(func, self._remove_proxy)
168+
except TypeError:
169+
proxy = _StrongRef(func)
243170
if proxy in self._func_cid_map[s]:
244171
return self._func_cid_map[s][proxy]
245172

246-
proxy.add_destroy_callback(self._remove_proxy)
247-
self._cid += 1
248-
cid = self._cid
173+
cid = next(self._cid_gen)
249174
self._func_cid_map[s][proxy] = cid
250-
self.callbacks.setdefault(s, dict())
175+
self.callbacks.setdefault(s, {})
251176
self.callbacks[s][cid] = proxy
252177
return cid
253178

@@ -257,7 +182,6 @@ def _remove_proxy(self, proxy):
257182
del self.callbacks[signal][proxies[proxy]]
258183
except KeyError:
259184
pass
260-
261185
if len(self.callbacks[signal]) == 0:
262186
del self.callbacks[signal]
263187
del self._func_cid_map[signal]
@@ -284,12 +208,11 @@ def process(self, s, *args, **kwargs):
284208
All of the functions registered to receive callbacks on *s* will be
285209
called with ``*args`` and ``**kwargs``.
286210
"""
287-
if s in self.callbacks:
288-
for cid, proxy in list(self.callbacks[s].items()):
211+
for cid, ref in list(self.callbacks.get(s, {}).items()):
212+
func = ref()
213+
if func is not None:
289214
try:
290-
proxy(*args, **kwargs)
291-
except ReferenceError:
292-
self._remove_proxy(proxy)
215+
func(*args, **kwargs)
293216
# this does not capture KeyboardInterrupt, SystemExit,
294217
# and GeneratorExit
295218
except Exception as exc:
@@ -979,10 +902,10 @@ class Grouper(object):
979902
980903
"""
981904
def __init__(self, init=()):
982-
self._mapping = {ref(x): [ref(x)] for x in init}
905+
self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init}
983906

984907
def __contains__(self, item):
985-
return ref(item) in self._mapping
908+
return weakref.ref(item) in self._mapping
986909

987910
def clean(self):
988911
"""Clean dead weak references from the dictionary."""
@@ -997,10 +920,10 @@ def join(self, a, *args):
997920
Join given arguments into the same set. Accepts one or more arguments.
998921
"""
999922
mapping = self._mapping
1000-
set_a = mapping.setdefault(ref(a), [ref(a)])
923+
set_a = mapping.setdefault(weakref.ref(a), [weakref.ref(a)])
1001924

1002925
for arg in args:
1003-
set_b = mapping.get(ref(arg), [ref(arg)])
926+
set_b = mapping.get(weakref.ref(arg), [weakref.ref(arg)])
1004927
if set_b is not set_a:
1005928
if len(set_b) > len(set_a):
1006929
set_a, set_b = set_b, set_a
@@ -1013,13 +936,14 @@ def join(self, a, *args):
1013936
def joined(self, a, b):
1014937
"""Returns True if *a* and *b* are members of the same set."""
1015938
self.clean()
1016-
return self._mapping.get(ref(a), object()) is self._mapping.get(ref(b))
939+
return (self._mapping.get(weakref.ref(a), object())
940+
is self._mapping.get(weakref.ref(b)))
1017941

1018942
def remove(self, a):
1019943
self.clean()
1020-
set_a = self._mapping.pop(ref(a), None)
944+
set_a = self._mapping.pop(weakref.ref(a), None)
1021945
if set_a:
1022-
set_a.remove(ref(a))
946+
set_a.remove(weakref.ref(a))
1023947

1024948
def __iter__(self):
1025949
"""
@@ -1035,7 +959,7 @@ def __iter__(self):
1035959
def get_siblings(self, a):
1036960
"""Returns all of the items joined with *a*, including itself."""
1037961
self.clean()
1038-
siblings = self._mapping.get(ref(a), [ref(a)])
962+
siblings = self._mapping.get(weakref.ref(a), [weakref.ref(a)])
1039963
return [x() for x in siblings]
1040964

1041965

@@ -1254,7 +1178,7 @@ def _compute_conf_interval(data, med, iqr, bootstrap):
12541178

12551179
ncols = len(X)
12561180
if labels is None:
1257-
labels = repeat(None)
1181+
labels = itertools.repeat(None)
12581182
elif len(labels) != ncols:
12591183
raise ValueError("Dimensions of labels and X must be compatible")
12601184

0 commit comments

Comments
 (0)