From 4ec886d77633249630792aaebe42d871b7cf785e Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Tue, 17 Feb 2015 21:32:47 +0100 Subject: [PATCH 1/6] Ensure connect() does not add duplicate callbacks. Fix if stmt. --- lib/matplotlib/cbook.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c0f66c9f27cd..ff34cd280073 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -361,6 +361,7 @@ class _BoundMethodProxy(object): Minor bugfixes by Michael Droettboom ''' def __init__(self, cb): + self._hash = hash(cb) try: try: self.inst = ref(cb.im_self) @@ -433,6 +434,9 @@ def __ne__(self, other): ''' return not self.__eq__(other) + def __hash__(self): + return self._hash + class CallbackRegistry(object): """ @@ -492,14 +496,14 @@ def connect(self, s, func): func will be called """ self._func_cid_map.setdefault(s, WeakKeyDictionary()) - if func in self._func_cid_map[s]: - return self._func_cid_map[s][func] + proxy = _BoundMethodProxy(func) + if proxy in self._func_cid_map[s]: + return self._func_cid_map[s][proxy] self._cid += 1 cid = self._cid - self._func_cid_map[s][func] = cid + self._func_cid_map[s][proxy] = cid self.callbacks.setdefault(s, dict()) - proxy = _BoundMethodProxy(func) self.callbacks[s][cid] = proxy return cid From 59f77aa888d0af746b45182b53bfc99e096bcca3 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Tue, 17 Feb 2015 21:46:59 +0100 Subject: [PATCH 2/6] Improved Garbage Collection --- lib/matplotlib/cbook.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index ff34cd280073..5e73ea97bdc0 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -362,9 +362,10 @@ class _BoundMethodProxy(object): ''' def __init__(self, cb): self._hash = hash(cb) + self._callbacks = [] try: try: - self.inst = ref(cb.im_self) + self.inst = ref(cb.im_self, self._destroy) except TypeError: self.inst = None if six.PY3: @@ -378,6 +379,13 @@ def __init__(self, cb): self.func = cb self.klass = None + def add_callback(self, callback): + self._callbacks.append(_BoundMethodProxy(callback)) + + def _destroy(self, wk): + for callback in self._callbacks: + callback(self) + def __getstate__(self): d = self.__dict__.copy() # de-weak reference inst @@ -500,6 +508,7 @@ def connect(self, s, func): if proxy in self._func_cid_map[s]: return self._func_cid_map[s][proxy] + proxy.add_callback(self.remove_proxy) # Remove the proxy when it dies. self._cid += 1 cid = self._cid self._func_cid_map[s][proxy] = cid @@ -507,6 +516,11 @@ def connect(self, s, func): self.callbacks[s][cid] = proxy return cid + def remove_proxy(self, proxy): + for category, proxies in list(six.iteritems(self._func_cid_map)): + if proxy in proxies: + del self.callbacks[category][proxies[proxy]] + def disconnect(self, cid): """ disconnect the callback registered with callback id *cid* @@ -531,11 +545,7 @@ def process(self, s, *args, **kwargs): """ if s in self.callbacks: for cid, proxy in list(six.iteritems(self.callbacks[s])): - # Clean out dead references - if proxy.inst is not None and proxy.inst() is None: - del self.callbacks[s][cid] - else: - proxy(*args, **kwargs) + proxy(*args, **kwargs) class Scheduler(threading.Thread): From 52f9e5a3190b9f25b5473bd5a21b02ba6c3af049 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 18 Feb 2015 11:43:10 +0100 Subject: [PATCH 3/6] Bug fix, speed-up, and safer-than-sorry. --- lib/matplotlib/cbook.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 5e73ea97bdc0..780c996c9350 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -384,7 +384,9 @@ def add_callback(self, callback): def _destroy(self, wk): for callback in self._callbacks: - callback(self) + try: + callback(self) + except ReferenceError: pass def __getstate__(self): d = self.__dict__.copy() @@ -508,7 +510,7 @@ def connect(self, s, func): if proxy in self._func_cid_map[s]: return self._func_cid_map[s][proxy] - proxy.add_callback(self.remove_proxy) # Remove the proxy when it dies. + proxy.add_callback(self._remove_proxy) # Remove the proxy when it dies. self._cid += 1 cid = self._cid self._func_cid_map[s][proxy] = cid @@ -516,10 +518,11 @@ def connect(self, s, func): self.callbacks[s][cid] = proxy return cid - def remove_proxy(self, proxy): + def _remove_proxy(self, proxy): for category, proxies in list(six.iteritems(self._func_cid_map)): - if proxy in proxies: + try: del self.callbacks[category][proxies[proxy]] + except KeyError: pass def disconnect(self, cid): """ @@ -545,7 +548,10 @@ def process(self, s, *args, **kwargs): """ if s in self.callbacks: for cid, proxy in list(six.iteritems(self.callbacks[s])): - proxy(*args, **kwargs) + try: + proxy(*args, **kwargs) + except ReferenceError: + self._remove_proxy(proxy) class Scheduler(threading.Thread): From 8864e5c2d22899ca111efcc269eddd299e5ef70c Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Fri, 20 Feb 2015 00:00:10 +0100 Subject: [PATCH 4/6] Nits --- lib/matplotlib/cbook.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 780c996c9350..b4b6de3277e4 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -362,7 +362,7 @@ class _BoundMethodProxy(object): ''' def __init__(self, cb): self._hash = hash(cb) - self._callbacks = [] + self._destroy_callbacks = [] try: try: self.inst = ref(cb.im_self, self._destroy) @@ -379,14 +379,15 @@ def __init__(self, cb): self.func = cb self.klass = None - def add_callback(self, callback): - self._callbacks.append(_BoundMethodProxy(callback)) + def add_destroy_callback(self, callback): + self._destroy_callbacks.append(_BoundMethodProxy(callback)) def _destroy(self, wk): - for callback in self._callbacks: + for callback in self._destroy_callbacks: try: callback(self) - except ReferenceError: pass + except ReferenceError: + pass def __getstate__(self): d = self.__dict__.copy() @@ -510,7 +511,7 @@ def connect(self, s, func): if proxy in self._func_cid_map[s]: return self._func_cid_map[s][proxy] - proxy.add_callback(self._remove_proxy) # Remove the proxy when it dies. + proxy.add_destroy_callback(self._remove_proxy) # Remove the proxy when it dies. self._cid += 1 cid = self._cid self._func_cid_map[s][proxy] = cid @@ -522,7 +523,8 @@ def _remove_proxy(self, proxy): for category, proxies in list(six.iteritems(self._func_cid_map)): try: del self.callbacks[category][proxies[proxy]] - except KeyError: pass + except KeyError: + pass def disconnect(self, cid): """ From d404e35eec80ee6799b606660e17e1237deca3d6 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Wed, 25 Feb 2015 23:11:44 +0100 Subject: [PATCH 5/6] Even better GC and tests! --- lib/matplotlib/cbook.py | 11 +++++-- lib/matplotlib/tests/test_cbook.py | 46 +++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index b4b6de3277e4..cb99da0d551c 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -520,12 +520,17 @@ def connect(self, s, func): return cid def _remove_proxy(self, proxy): - for category, proxies in list(six.iteritems(self._func_cid_map)): + for signal, proxies in list(six.iteritems(self._func_cid_map)): try: - del self.callbacks[category][proxies[proxy]] + del self.callbacks[signal][proxies[proxy]] except KeyError: pass + if len(self.callbacks[signal]) == 0: + del self.callbacks[signal] + del self._func_cid_map[signal] + + def disconnect(self, cid): """ disconnect the callback registered with callback id *cid* @@ -536,7 +541,7 @@ def disconnect(self, cid): except KeyError: continue else: - for category, functions in list( + for signal, functions in list( six.iteritems(self._func_cid_map)): for function, value in list(six.iteritems(functions)): if value == cid: diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 416fa0c74020..0965a3d18066 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -8,7 +8,7 @@ import numpy as np from numpy.testing.utils import (assert_array_equal, assert_approx_equal, assert_array_almost_equal) -from nose.tools import assert_equal, raises, assert_true +from nose.tools import assert_equal, assert_not_equal, raises, assert_true import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -243,3 +243,47 @@ def test_label_error(self): def test_bad_dims(self): data = np.random.normal(size=(34, 34, 34)) results = cbook.boxplot_stats(data) + + +class Test_callback_registry(object): + def setup(self): + self.signal = 'test' + self.callbacks = cbook.CallbackRegistry() + + def connect(self, s, func): + return self.callbacks.connect(s, func) + + def is_empty(self): + assert_equal(self.callbacks._func_cid_map, {}) + assert_equal(self.callbacks.callbacks, {}) + + def is_not_empty(self): + assert_not_equal(self.callbacks._func_cid_map, {}) + assert_not_equal(self.callbacks.callbacks, {}) + + def test_callback_complete(self): + # ensure we start with an empty registry + self.is_empty() + + # create a class for testing + mini_me = Test_callback_registry() + + # test that we can add a callback + cid1 = self.connect(self.signal, mini_me.dummy) + assert_equal(type(cid1), int) + self.is_not_empty() + + # test that we don't add a second callback + cid2 = self.connect(self.signal, mini_me.dummy) + assert_equal(cid1, cid2) + self.is_not_empty() + assert_equal(len(self.callbacks._func_cid_map), 1) + assert_equal(len(self.callbacks.callbacks), 1) + + del mini_me + + # check we now have no callbacks registered + self.is_empty() + + def dummy(self): + pass From 03d97f239c2cff6467b9e80e951ea429d96c3be6 Mon Sep 17 00:00:00 2001 From: OceanWolf Date: Thu, 26 Feb 2015 13:06:10 +0100 Subject: [PATCH 6/6] Improved GC for python3. --- lib/matplotlib/cbook.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index cb99da0d551c..814df14d9ab4 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -365,7 +365,10 @@ def __init__(self, cb): self._destroy_callbacks = [] try: try: - self.inst = ref(cb.im_self, self._destroy) + if six.PY3: + self.inst = ref(cb.__self__, self._destroy) + else: + self.inst = ref(cb.im_self, self._destroy) except TypeError: self.inst = None if six.PY3: @@ -507,11 +510,13 @@ def connect(self, s, func): func will be called """ self._func_cid_map.setdefault(s, WeakKeyDictionary()) + # Note proxy not needed in python 3. + # TODO rewrite this when support for python2.x gets dropped. proxy = _BoundMethodProxy(func) if proxy in self._func_cid_map[s]: return self._func_cid_map[s][proxy] - proxy.add_destroy_callback(self._remove_proxy) # Remove the proxy when it dies. + proxy.add_destroy_callback(self._remove_proxy) self._cid += 1 cid = self._cid self._func_cid_map[s][proxy] = cid