@@ -117,6 +117,61 @@ def _weak_or_strong_ref(func, callback):
117117 return _StrongRef (func )
118118
119119
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+
120175class CallbackRegistry :
121176 """
122177 Handle registering, processing, blocking, and disconnecting
@@ -176,14 +231,14 @@ class CallbackRegistry:
176231
177232 # We maintain two mappings:
178233 # callbacks: signal -> {cid -> weakref-to-callback}
179- # _func_cid_map: signal -> { weakref-to-callback -> cid}
234+ # _func_cid_map: {( signal, weakref-to-callback) -> cid}
180235
181236 def __init__ (self , exception_handler = _exception_printer , * , signals = None ):
182237 self ._signals = None if signals is None else list (signals ) # Copy it.
183238 self .exception_handler = exception_handler
184239 self .callbacks = {}
185240 self ._cid_gen = itertools .count ()
186- self ._func_cid_map = {}
241+ self ._func_cid_map = _UnhashDict ([])
187242 # A hidden variable that marks cids that need to be pickled.
188243 self ._pickled_cids = set ()
189244
@@ -204,27 +259,25 @@ def __setstate__(self, state):
204259 cid_count = state .pop ('_cid_gen' )
205260 vars (self ).update (state )
206261 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 ) )
208263 for cid , func in d .items ()}
209264 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 ())
213268 self ._cid_gen = itertools .count (cid_count )
214269
215270 def connect (self , signal , func ):
216271 """Register *func* to be called when signal *signal* is generated."""
217272 if self ._signals is not None :
218273 _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
228281
229282 def _connect_picklable (self , signal , func ):
230283 """
@@ -238,23 +291,18 @@ def _connect_picklable(self, signal, func):
238291
239292 # Keep a reference to sys.is_finalizing, as sys may have been cleared out
240293 # 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 ):
242295 if _is_finalizing ():
243296 # Weakrefs can't be properly torn down at that point anymore.
244297 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
253303 return
254- # Clean up empty dicts
255- if len (self .callbacks [signal ]) == 0 :
304+ if len (self .callbacks [signal ]) == 0 : # Clean up empty dicts
256305 del self .callbacks [signal ]
257- del self ._func_cid_map [signal ]
258306
259307 def disconnect (self , cid ):
260308 """
@@ -263,24 +311,16 @@ def disconnect(self, cid):
263311 No error is raised if such a callback does not exist.
264312 """
265313 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 :
270316 break
271- else :
272- # Not found
317+ else : # Not found
273318 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
282323 del self .callbacks [signal ]
283- del self ._func_cid_map [signal ]
284324
285325 def process (self , s , * args , ** kwargs ):
286326 """
0 commit comments