Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fb6b895

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 fb6b895

File tree

1 file changed

+47
-130
lines changed

1 file changed

+47
-130
lines changed

lib/matplotlib/cbook/__init__.py

Lines changed: 47 additions & 130 deletions
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,20 @@ 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
130-
131-
mtd = types.MethodType(self.func, self.inst())
132-
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)
138-
139-
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
151-
152-
def __hash__(self):
153-
return self._hash
15474

75+
def __init__(self, obj):
76+
self._obj = obj
15577

156-
def _exception_printer(exc):
157-
traceback.print_exc()
78+
def __call__(self):
79+
return self._obj
15880

15981

16082
class CallbackRegistry(object):
@@ -179,20 +101,13 @@ class CallbackRegistry(object):
179101
>>> callbacks.disconnect(id_eat)
180102
>>> callbacks.process('eat', 456) # nothing will be called
181103
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-
104+
In practice, one should always disconnect all callbacks when they are
105+
no longer needed to avoid dangling references (and thus memory leaks).
106+
However, real code in Matplotlib rarely does so, and due to its design,
107+
it is rather difficult to place this kind of code. To get around this,
108+
and prevent this class of memory leaks, we instead store weak references
109+
to bound methods only, so when the destination object needs to die, the
110+
CallbackRegistry won't keep it alive.
196111
197112
Parameters
198113
----------
@@ -211,12 +126,17 @@ def handler(exc: Exception) -> None:
211126
212127
def h(exc):
213128
traceback.print_exc()
214-
215129
"""
130+
131+
# We maintain two mappings:
132+
# callbacks: signal -> {cid -> callback}
133+
# _func_cid_map: signal -> {callback -> cid}
134+
# (actually, callbacks are weakrefs to the actual callbacks).
135+
216136
def __init__(self, exception_handler=_exception_printer):
217137
self.exception_handler = exception_handler
218138
self.callbacks = dict()
219-
self._cid = 0
139+
self._cid_gen = itertools.count()
220140
self._func_cid_map = {}
221141

222142
# In general, callbacks may not be pickled; thus, we simply recreate an
@@ -236,18 +156,16 @@ def __setstate__(self, state):
236156
def connect(self, s, func):
237157
"""Register *func* to be called when signal *s* is generated.
238158
"""
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)
159+
self._func_cid_map.setdefault(s, {})
160+
proxy = (WeakMethod(func, self._remove_proxy)
161+
if inspect.ismethod(func)
162+
else _StrongRef(func))
243163
if proxy in self._func_cid_map[s]:
244164
return self._func_cid_map[s][proxy]
245165

246-
proxy.add_destroy_callback(self._remove_proxy)
247-
self._cid += 1
248-
cid = self._cid
166+
cid = next(self._cid_gen)
249167
self._func_cid_map[s][proxy] = cid
250-
self.callbacks.setdefault(s, dict())
168+
self.callbacks.setdefault(s, {})
251169
self.callbacks[s][cid] = proxy
252170
return cid
253171

@@ -257,7 +175,6 @@ def _remove_proxy(self, proxy):
257175
del self.callbacks[signal][proxies[proxy]]
258176
except KeyError:
259177
pass
260-
261178
if len(self.callbacks[signal]) == 0:
262179
del self.callbacks[signal]
263180
del self._func_cid_map[signal]
@@ -284,12 +201,11 @@ def process(self, s, *args, **kwargs):
284201
All of the functions registered to receive callbacks on *s* will be
285202
called with ``*args`` and ``**kwargs``.
286203
"""
287-
if s in self.callbacks:
288-
for cid, proxy in list(self.callbacks[s].items()):
204+
for cid, ref in list(self.callbacks.get(s, {}).items()):
205+
func = ref()
206+
if func is not None:
289207
try:
290-
proxy(*args, **kwargs)
291-
except ReferenceError:
292-
self._remove_proxy(proxy)
208+
func(*args, **kwargs)
293209
# this does not capture KeyboardInterrupt, SystemExit,
294210
# and GeneratorExit
295211
except Exception as exc:
@@ -979,10 +895,10 @@ class Grouper(object):
979895
980896
"""
981897
def __init__(self, init=()):
982-
self._mapping = {ref(x): [ref(x)] for x in init}
898+
self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init}
983899

984900
def __contains__(self, item):
985-
return ref(item) in self._mapping
901+
return weakref.ref(item) in self._mapping
986902

987903
def clean(self):
988904
"""Clean dead weak references from the dictionary."""
@@ -997,10 +913,10 @@ def join(self, a, *args):
997913
Join given arguments into the same set. Accepts one or more arguments.
998914
"""
999915
mapping = self._mapping
1000-
set_a = mapping.setdefault(ref(a), [ref(a)])
916+
set_a = mapping.setdefault(weakref.ref(a), [weakref.ref(a)])
1001917

1002918
for arg in args:
1003-
set_b = mapping.get(ref(arg), [ref(arg)])
919+
set_b = mapping.get(weakref.ref(arg), [weakref.ref(arg)])
1004920
if set_b is not set_a:
1005921
if len(set_b) > len(set_a):
1006922
set_a, set_b = set_b, set_a
@@ -1013,13 +929,14 @@ def join(self, a, *args):
1013929
def joined(self, a, b):
1014930
"""Returns True if *a* and *b* are members of the same set."""
1015931
self.clean()
1016-
return self._mapping.get(ref(a), object()) is self._mapping.get(ref(b))
932+
return (self._mapping.get(weakref.ref(a), object())
933+
is self._mapping.get(weakref.ref(b)))
1017934

1018935
def remove(self, a):
1019936
self.clean()
1020-
set_a = self._mapping.pop(ref(a), None)
937+
set_a = self._mapping.pop(weakref.ref(a), None)
1021938
if set_a:
1022-
set_a.remove(ref(a))
939+
set_a.remove(weakref.ref(a))
1023940

1024941
def __iter__(self):
1025942
"""
@@ -1035,7 +952,7 @@ def __iter__(self):
1035952
def get_siblings(self, a):
1036953
"""Returns all of the items joined with *a*, including itself."""
1037954
self.clean()
1038-
siblings = self._mapping.get(ref(a), [ref(a)])
955+
siblings = self._mapping.get(weakref.ref(a), [weakref.ref(a)])
1039956
return [x() for x in siblings]
1040957

1041958

@@ -1254,7 +1171,7 @@ def _compute_conf_interval(data, med, iqr, bootstrap):
12541171

12551172
ncols = len(X)
12561173
if labels is None:
1257-
labels = repeat(None)
1174+
labels = itertools.repeat(None)
12581175
elif len(labels) != ncols:
12591176
raise ValueError("Dimensions of labels and X must be compatible")
12601177

0 commit comments

Comments
 (0)