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

Skip to content

Commit 7dc4990

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 ef71b49 commit 7dc4990

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

@@ -200,9 +255,9 @@ def __setstate__(self, state):
200255
s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s))
201256
for cid, func in d.items()}
202257
for s, d in self.callbacks.items()}
203-
self._func_cid_map = {
204-
(s, proxy): cid
205-
for s, d in self.callbacks.items() for cid, proxy in d.items()}
258+
self._func_cid_map = _UnhashDict(
259+
((s, proxy), cid)
260+
for s, d in self.callbacks.items() for cid, proxy in d.items())
206261

207262
def connect(self, signal, func):
208263
"""Register *func* to be called when signal *signal* is generated."""
@@ -255,7 +310,7 @@ def disconnect(self, cid):
255310
return
256311
assert self.callbacks[signal][cid] == proxy
257312
del self.callbacks[signal][cid]
258-
del self._func_cid_map[signal, proxy]
313+
self._func_cid_map.pop((signal, proxy))
259314
if len(self.callbacks[signal]) == 0: # Clean up empty dicts
260315
del self.callbacks[signal]
261316

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,22 +209,23 @@ 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
@pytest.mark.parametrize('pickle', [True, False])
213-
def test_callback_complete(self, pickle):
222+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
223+
def test_callback_complete(self, pickle, cls):
214224
# ensure we start with an empty registry
215225
self.is_empty()
216226

217227
# create a class for testing
218-
mini_me = Test_callback_registry()
228+
mini_me = cls()
219229

220230
# test that we can add a callback
221231
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -226,7 +236,7 @@ def test_callback_complete(self, pickle):
226236
cid2 = self.connect(self.signal, mini_me.dummy, pickle)
227237
assert cid1 == cid2
228238
self.is_not_empty()
229-
assert len(self.callbacks._func_cid_map) == 1
239+
assert len([*self.callbacks._func_cid_map]) == 1
230240
assert len(self.callbacks.callbacks) == 1
231241

232242
del mini_me
@@ -235,12 +245,13 @@ def test_callback_complete(self, pickle):
235245
self.is_empty()
236246

237247
@pytest.mark.parametrize('pickle', [True, False])
238-
def test_callback_disconnect(self, pickle):
248+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
249+
def test_callback_disconnect(self, pickle, cls):
239250
# ensure we start with an empty registry
240251
self.is_empty()
241252

242253
# create a class for testing
243-
mini_me = Test_callback_registry()
254+
mini_me = cls()
244255

245256
# test that we can add a callback
246257
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -253,12 +264,13 @@ def test_callback_disconnect(self, pickle):
253264
self.is_empty()
254265

255266
@pytest.mark.parametrize('pickle', [True, False])
256-
def test_callback_wrong_disconnect(self, pickle):
267+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
268+
def test_callback_wrong_disconnect(self, pickle, cls):
257269
# ensure we start with an empty registry
258270
self.is_empty()
259271

260272
# create a class for testing
261-
mini_me = Test_callback_registry()
273+
mini_me = cls()
262274

263275
# test that we can add a callback
264276
cid1 = self.connect(self.signal, mini_me.dummy, pickle)
@@ -271,20 +283,21 @@ def test_callback_wrong_disconnect(self, pickle):
271283
self.is_not_empty()
272284

273285
@pytest.mark.parametrize('pickle', [True, False])
274-
def test_registration_on_non_empty_registry(self, pickle):
286+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
287+
def test_registration_on_non_empty_registry(self, pickle, cls):
275288
# ensure we start with an empty registry
276289
self.is_empty()
277290

278291
# setup the registry with a callback
279-
mini_me = Test_callback_registry()
292+
mini_me = cls()
280293
self.connect(self.signal, mini_me.dummy, pickle)
281294

282295
# Add another callback
283-
mini_me2 = Test_callback_registry()
296+
mini_me2 = cls()
284297
self.connect(self.signal, mini_me2.dummy, pickle)
285298

286299
# Remove and add the second callback
287-
mini_me2 = Test_callback_registry()
300+
mini_me2 = cls()
288301
self.connect(self.signal, mini_me2.dummy, pickle)
289302

290303
# We still have 2 references
@@ -296,9 +309,6 @@ def test_registration_on_non_empty_registry(self, pickle):
296309
mini_me2 = None
297310
self.is_empty()
298311

299-
def dummy(self):
300-
pass
301-
302312
def test_pickling(self):
303313
assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())),
304314
"callbacks")

0 commit comments

Comments
 (0)