@@ -117,6 +117,61 @@ def _weak_or_strong_ref(func, callback):
117
117
return _StrongRef (func )
118
118
119
119
120
+ class _UnhashDict :
121
+ """
122
+ A minimal dict-like class that also supports unhashable keys, storing them
123
+ in a list of key-value pairs.
124
+
125
+ This class only implements the interface needed for `CallbackRegistry`, and
126
+ tries to minimize the overhead for the hashable case.
127
+ """
128
+
129
+ def __init__ (self , pairs ):
130
+ self ._dict = {}
131
+ self ._pairs = []
132
+ for k , v in pairs :
133
+ self [k ] = v
134
+
135
+ def __setitem__ (self , key , value ):
136
+ try :
137
+ self ._dict [key ] = value
138
+ except TypeError :
139
+ for i , (k , v ) in enumerate (self ._pairs ):
140
+ if k == key :
141
+ self ._pairs [i ] = (key , value )
142
+ break
143
+ else :
144
+ self ._pairs .append ((key , value ))
145
+
146
+ def __getitem__ (self , key ):
147
+ try :
148
+ return self ._dict [key ]
149
+ except TypeError :
150
+ pass
151
+ for k , v in self ._pairs :
152
+ if k == key :
153
+ return v
154
+ raise KeyError (key )
155
+
156
+ def pop (self , key , * args ):
157
+ try :
158
+ if key in self ._dict :
159
+ return self ._dict .pop (key )
160
+ except TypeError :
161
+ for i , (k , v ) in enumerate (self ._pairs ):
162
+ if k == key :
163
+ del self ._pairs [i ]
164
+ return v
165
+ if args :
166
+ return args [0 ]
167
+ raise KeyError (key )
168
+
169
+ def __iter__ (self ):
170
+ yield from self ._dict
171
+ for k , v in self ._pairs :
172
+ yield k
173
+
174
+
120
175
class CallbackRegistry :
121
176
"""
122
177
Handle registering, processing, blocking, and disconnecting
@@ -176,14 +231,14 @@ class CallbackRegistry:
176
231
177
232
# We maintain two mappings:
178
233
# callbacks: signal -> {cid -> weakref-to-callback}
179
- # _func_cid_map: signal -> { weakref-to-callback -> cid}
234
+ # _func_cid_map: {( signal, weakref-to-callback) -> cid}
180
235
181
236
def __init__ (self , exception_handler = _exception_printer , * , signals = None ):
182
237
self ._signals = None if signals is None else list (signals ) # Copy it.
183
238
self .exception_handler = exception_handler
184
239
self .callbacks = {}
185
240
self ._cid_gen = itertools .count ()
186
- self ._func_cid_map = {}
241
+ self ._func_cid_map = _UnhashDict ([])
187
242
# A hidden variable that marks cids that need to be pickled.
188
243
self ._pickled_cids = set ()
189
244
@@ -204,27 +259,25 @@ def __setstate__(self, state):
204
259
cid_count = state .pop ('_cid_gen' )
205
260
vars (self ).update (state )
206
261
self .callbacks = {
207
- s : {cid : _weak_or_strong_ref (func , self ._remove_proxy )
262
+ s : {cid : _weak_or_strong_ref (func , functools . partial ( self ._remove_proxy , s ) )
208
263
for cid , func in d .items ()}
209
264
for s , d in self .callbacks .items ()}
210
- self ._func_cid_map = {
211
- s : { proxy : cid for cid , proxy in d . items ()}
212
- for s , d in self .callbacks .items ()}
265
+ self ._func_cid_map = _UnhashDict (
266
+ (( s , proxy ), cid )
267
+ for s , d in self .callbacks .items () for cid , proxy in d . items ())
213
268
self ._cid_gen = itertools .count (cid_count )
214
269
215
270
def connect (self , signal , func ):
216
271
"""Register *func* to be called when signal *signal* is generated."""
217
272
if self ._signals is not None :
218
273
_api .check_in_list (self ._signals , signal = signal )
219
- self ._func_cid_map .setdefault (signal , {})
220
- proxy = _weak_or_strong_ref (func , self ._remove_proxy )
221
- if proxy in self ._func_cid_map [signal ]:
222
- return self ._func_cid_map [signal ][proxy ]
223
- cid = next (self ._cid_gen )
224
- self ._func_cid_map [signal ][proxy ] = cid
225
- self .callbacks .setdefault (signal , {})
226
- self .callbacks [signal ][cid ] = proxy
227
- return cid
274
+ proxy = _weak_or_strong_ref (func , functools .partial (self ._remove_proxy , signal ))
275
+ try :
276
+ return self ._func_cid_map [signal , proxy ]
277
+ except KeyError :
278
+ cid = self ._func_cid_map [signal , proxy ] = next (self ._cid_gen )
279
+ self .callbacks .setdefault (signal , {})[cid ] = proxy
280
+ return cid
228
281
229
282
def _connect_picklable (self , signal , func ):
230
283
"""
@@ -238,23 +291,18 @@ def _connect_picklable(self, signal, func):
238
291
239
292
# Keep a reference to sys.is_finalizing, as sys may have been cleared out
240
293
# at that point.
241
- def _remove_proxy (self , proxy , * , _is_finalizing = sys .is_finalizing ):
294
+ def _remove_proxy (self , signal , proxy , * , _is_finalizing = sys .is_finalizing ):
242
295
if _is_finalizing ():
243
296
# Weakrefs can't be properly torn down at that point anymore.
244
297
return
245
- for signal , proxy_to_cid in list (self ._func_cid_map .items ()):
246
- cid = proxy_to_cid .pop (proxy , None )
247
- if cid is not None :
248
- del self .callbacks [signal ][cid ]
249
- self ._pickled_cids .discard (cid )
250
- break
251
- else :
252
- # Not found
298
+ cid = self ._func_cid_map .pop ((signal , proxy ), None )
299
+ if cid is not None :
300
+ del self .callbacks [signal ][cid ]
301
+ self ._pickled_cids .discard (cid )
302
+ else : # Not found
253
303
return
254
- # Clean up empty dicts
255
- if len (self .callbacks [signal ]) == 0 :
304
+ if len (self .callbacks [signal ]) == 0 : # Clean up empty dicts
256
305
del self .callbacks [signal ]
257
- del self ._func_cid_map [signal ]
258
306
259
307
def disconnect (self , cid ):
260
308
"""
@@ -263,24 +311,16 @@ def disconnect(self, cid):
263
311
No error is raised if such a callback does not exist.
264
312
"""
265
313
self ._pickled_cids .discard (cid )
266
- # Clean up callbacks
267
- for signal , cid_to_proxy in list (self .callbacks .items ()):
268
- proxy = cid_to_proxy .pop (cid , None )
269
- if proxy is not None :
314
+ for signal , proxy in self ._func_cid_map :
315
+ if self ._func_cid_map [signal , proxy ] == cid :
270
316
break
271
- else :
272
- # Not found
317
+ else : # Not found
273
318
return
274
-
275
- proxy_to_cid = self ._func_cid_map [signal ]
276
- for current_proxy , current_cid in list (proxy_to_cid .items ()):
277
- if current_cid == cid :
278
- assert proxy is current_proxy
279
- del proxy_to_cid [current_proxy ]
280
- # Clean up empty dicts
281
- if len (self .callbacks [signal ]) == 0 :
319
+ assert self .callbacks [signal ][cid ] == proxy
320
+ del self .callbacks [signal ][cid ]
321
+ self ._func_cid_map .pop ((signal , proxy ))
322
+ if len (self .callbacks [signal ]) == 0 : # Clean up empty dicts
282
323
del self .callbacks [signal ]
283
- del self ._func_cid_map [signal ]
284
324
285
325
def process (self , s , * args , ** kwargs ):
286
326
"""
0 commit comments