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

Skip to content

Commit 87f194f

Browse files
committed
Support unhashable callbacks in CallbackRegistry.
... by replacing _func_cid_map by a dict-like structure (_UnhashDict) that also supports unhashable entries. Note that _func_cid_map (and thus _UnhashDict) can be dropped if we get rid of proxy deduplication in CallbackRegistry.
1 parent 920a6d2 commit 87f194f

File tree

2 files changed

+86
-21
lines changed

2 files changed

+86
-21
lines changed

lib/matplotlib/cbook.py

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,61 @@ def _weak_or_strong_ref(func, callback):
112112
return _StrongRef(func)
113113

114114

115+
class _UnhashDict:
116+
"""
117+
A minimal dict-like class that also supports unhashable keys, storing them
118+
in a list of key-value pairs.
119+
120+
This class only implements the interface needed for `CallbackRegistry`, and
121+
tries to minimize the overhead for the hashable case.
122+
"""
123+
124+
def __init__(self, pairs):
125+
self._dict = {}
126+
self._pairs = []
127+
for k, v in pairs:
128+
self[k] = v
129+
130+
def __setitem__(self, key, value):
131+
try:
132+
self._dict[key] = value
133+
except TypeError:
134+
for i, (k, v) in enumerate(self._pairs):
135+
if k == key:
136+
self._pairs[i] = (key, value)
137+
break
138+
else:
139+
self._pairs.append((key, value))
140+
141+
def __getitem__(self, key):
142+
try:
143+
return self._dict[key]
144+
except TypeError:
145+
pass
146+
for k, v in self._pairs:
147+
if k == key:
148+
return v
149+
raise KeyError(key)
150+
151+
def pop(self, key, *args):
152+
try:
153+
if key in self._dict:
154+
return self._dict.pop(key)
155+
except TypeError:
156+
for i, (k, v) in enumerate(self._pairs):
157+
if k == key:
158+
del self._pairs[i]
159+
return v
160+
if args:
161+
return args[0]
162+
raise KeyError(key)
163+
164+
def __iter__(self):
165+
yield from self._dict
166+
for k, v in self._pairs:
167+
yield k
168+
169+
115170
class CallbackRegistry:
116171
"""
117172
Handle registering, processing, blocking, and disconnecting
@@ -178,7 +233,7 @@ def __init__(self, exception_handler=_exception_printer, *, signals=None):
178233
self.exception_handler = exception_handler
179234
self.callbacks = {}
180235
self._cid_gen = itertools.count()
181-
self._func_cid_map = {}
236+
self._func_cid_map = _UnhashDict([])
182237
# A hidden variable that marks cids that need to be pickled.
183238
self._pickled_cids = set()
184239

@@ -202,9 +257,9 @@ def __setstate__(self, state):
202257
s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s))
203258
for cid, func in d.items()}
204259
for s, d in self.callbacks.items()}
205-
self._func_cid_map = {
206-
(s, proxy): cid
207-
for s, d in self.callbacks.items() for cid, proxy in d.items()}
260+
self._func_cid_map = _UnhashDict(
261+
((s, proxy), cid)
262+
for s, d in self.callbacks.items() for cid, proxy in d.items())
208263
self._cid_gen = itertools.count(cid_count)
209264

210265
def connect(self, signal, func):
@@ -258,7 +313,7 @@ def disconnect(self, cid):
258313
return
259314
assert self.callbacks[signal][cid] == proxy
260315
del self.callbacks[signal][cid]
261-
del self._func_cid_map[signal, proxy]
316+
self._func_cid_map.pop((signal, proxy))
262317
if len(self.callbacks[signal]) == 0: # Clean up empty dicts
263318
del self.callbacks[signal]
264319

lib/matplotlib/tests/test_cbook.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ def test_boxplot_stats_autorange_false(self):
178178
assert_array_almost_equal(bstats_true[0]['fliers'], [])
179179

180180

181+
class Hashable:
182+
def dummy(self): pass
183+
184+
185+
class Unhashable:
186+
__hash__ = None # type: ignore
187+
def dummy(self): pass
188+
189+
181190
class Test_callback_registry:
182191
def setup_method(self):
183192
self.signal = 'test'
@@ -200,13 +209,13 @@ def count(self):
200209

201210
def is_empty(self):
202211
np.testing.break_cycles()
203-
assert self.callbacks._func_cid_map == {}
212+
assert [*self.callbacks._func_cid_map] == []
204213
assert self.callbacks.callbacks == {}
205214
assert self.callbacks._pickled_cids == set()
206215

207216
def is_not_empty(self):
208217
np.testing.break_cycles()
209-
assert self.callbacks._func_cid_map != {}
218+
assert [*self.callbacks._func_cid_map] != []
210219
assert self.callbacks.callbacks != {}
211220

212221
def test_cid_restore(self):
@@ -217,12 +226,13 @@ def test_cid_restore(self):
217226
assert cid == 1
218227

219228
@pytest.mark.parametrize('pickle', [True, False])
220-
def test_callback_complete(self, pickle):
229+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
230+
def test_callback_complete(self, pickle, cls):
221231
# ensure we start with an empty registry
222232
self.is_empty()
223233

224234
# create a class for testing
225-
mini_me = Test_callback_registry()
235+
mini_me = cls()
226236

227237
# test that we can add a callback
228238
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -233,7 +243,7 @@ def test_callback_complete(self, pickle):
233243
cid2 = self.connect(self.signal, mini_me.dummy, pickle)
234244
assert cid1 == cid2
235245
self.is_not_empty()
236-
assert len(self.callbacks._func_cid_map) == 1
246+
assert len([*self.callbacks._func_cid_map]) == 1
237247
assert len(self.callbacks.callbacks) == 1
238248

239249
del mini_me
@@ -242,12 +252,13 @@ def test_callback_complete(self, pickle):
242252
self.is_empty()
243253

244254
@pytest.mark.parametrize('pickle', [True, False])
245-
def test_callback_disconnect(self, pickle):
255+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
256+
def test_callback_disconnect(self, pickle, cls):
246257
# ensure we start with an empty registry
247258
self.is_empty()
248259

249260
# create a class for testing
250-
mini_me = Test_callback_registry()
261+
mini_me = cls()
251262

252263
# test that we can add a callback
253264
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -260,12 +271,13 @@ def test_callback_disconnect(self, pickle):
260271
self.is_empty()
261272

262273
@pytest.mark.parametrize('pickle', [True, False])
263-
def test_callback_wrong_disconnect(self, pickle):
274+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
275+
def test_callback_wrong_disconnect(self, pickle, cls):
264276
# ensure we start with an empty registry
265277
self.is_empty()
266278

267279
# create a class for testing
268-
mini_me = Test_callback_registry()
280+
mini_me = cls()
269281

270282
# test that we can add a callback
271283
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -278,20 +290,21 @@ def test_callback_wrong_disconnect(self, pickle):
278290
self.is_not_empty()
279291

280292
@pytest.mark.parametrize('pickle', [True, False])
281-
def test_registration_on_non_empty_registry(self, pickle):
293+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
294+
def test_registration_on_non_empty_registry(self, pickle, cls):
282295
# ensure we start with an empty registry
283296
self.is_empty()
284297

285298
# setup the registry with a callback
286-
mini_me = Test_callback_registry()
299+
mini_me = cls()
287300
self.connect(self.signal, mini_me.dummy, pickle)
288301

289302
# Add another callback
290-
mini_me2 = Test_callback_registry()
303+
mini_me2 = cls()
291304
self.connect(self.signal, mini_me2.dummy, pickle)
292305

293306
# Remove and add the second callback
294-
mini_me2 = Test_callback_registry()
307+
mini_me2 = cls()
295308
self.connect(self.signal, mini_me2.dummy, pickle)
296309

297310
# We still have 2 references
@@ -303,9 +316,6 @@ def test_registration_on_non_empty_registry(self, pickle):
303316
mini_me2 = None
304317
self.is_empty()
305318

306-
def dummy(self):
307-
pass
308-
309319
def test_pickling(self):
310320
assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())),
311321
"callbacks")

0 commit comments

Comments
 (0)