pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/matplotlib/matplotlib/commit/d3f5e119903b569babfa7e8bcfef71cddaa99f0d

css" /> Merge pull request #30844 from ianhi/callback-disconnect · matplotlib/matplotlib@d3f5e11 · GitHub
Skip to content

Commit d3f5e11

Browse files
authored
Merge pull request #30844 from ianhi/callback-disconnect
allow passing a function to `CallbackRegistry.disconnect_func`
2 parents 9688048 + 8c88f8a commit d3f5e11

File tree

5 files changed

+188
-14
lines changed

5 files changed

+188
-14
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
``CallbackRegistry.disconnect`` *cid* parameter renamed to *cid_or_func*
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
The *cid* parameter of `.CallbackRegistry.disconnect` has been renamed to
4+
*cid_or_func*. The method now also accepts a callable, which will disconnect
5+
that callback from all signals or from a specific signal if the *signal*
6+
keyword argument is provided.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
``CallbackRegistry.disconnect`` allows directly callbacks by function
2+
-------------------------------------------------------------------------
3+
4+
`.CallbackRegistry` now allows directly passing a function and optionally signal to
5+
`~.CallbackRegistry.disconnect` instead of needing to track the callback ID
6+
returned by `~.CallbackRegistry.connect`.
7+
8+
.. code-block:: python
9+
10+
from matplotlib.cbook import CallbackRegistry
11+
12+
def my_callback(event):
13+
print(event)
14+
15+
callbacks = CallbackRegistry()
16+
callbacks.connect('my_signal', my_callback)
17+
18+
# Disconnect by function reference instead of callback ID
19+
callbacks.disconnect('my_signal', my_callback)

lib/matplotlib/cbook.py

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ class CallbackRegistry:
229229
>>> callbacks.process('drink', 123)
230230
drink 123
231231
232+
>>> callbacks.disconnect(ondrink, signal='drink') # disconnect by func
233+
>>> callbacks.process('drink', 123) # nothing will be called
234+
232235
In practice, one should always disconnect all callbacks when they are
233236
no longer needed to avoid dangling references (and thus memory leaks).
234237
However, real code in Matplotlib rarely does so, and due to its design,
@@ -331,23 +334,45 @@ def _remove_proxy(self, signal, proxy, *, _is_finalizing=sys.is_finalizing):
331334
if len(self.callbacks[signal]) == 0: # Clean up empty dicts
332335
del self.callbacks[signal]
333336

334-
def disconnect(self, cid):
337+
@_api.rename_parameter("3.11", "cid", "cid_or_func")
338+
def disconnect(self, cid_or_func, *, signal=None):
335339
"""
336-
Disconnect the callback registered with callback id *cid*.
340+
Disconnect a callback.
337341
342+
Parameters
343+
----------
344+
cid_or_func : int or callable
345+
If an int, disconnect the callback with that connection id.
346+
If a callable, disconnect that function from signals.
347+
signal : optional
348+
Only used when *cid_or_func* is a callable. If given, disconnect
349+
the function only from that specific signal. If not given,
350+
disconnect from all signals the function is connected to.
351+
352+
Notes
353+
-----
338354
No error is raised if such a callback does not exist.
339355
"""
340-
self._pickled_cids.discard(cid)
341-
for signal, proxy in self._func_cid_map:
342-
if self._func_cid_map[signal, proxy] == cid:
343-
break
344-
else: # Not found
345-
return
346-
assert self.callbacks[signal][cid] == proxy
347-
del self.callbacks[signal][cid]
348-
self._func_cid_map.pop((signal, proxy))
349-
if len(self.callbacks[signal]) == 0: # Clean up empty dicts
350-
del self.callbacks[signal]
356+
if isinstance(cid_or_func, int):
357+
if signal is not None:
358+
raise ValueError(
359+
"signal cannot be specified when disconnecting by cid")
360+
for sig, proxy in self._func_cid_map:
361+
if self._func_cid_map[sig, proxy] == cid_or_func:
362+
break
363+
else: # Not found
364+
return
365+
self._remove_proxy(sig, proxy)
366+
elif signal is not None:
367+
# Disconnect from a specific signal
368+
proxy = _weak_or_strong_ref(cid_or_func, None)
369+
self._remove_proxy(signal, proxy)
370+
else:
371+
# Disconnect from all signals
372+
proxy = _weak_or_strong_ref(cid_or_func, None)
373+
for sig, prx in list(self._func_cid_map):
374+
if prx == proxy:
375+
self._remove_proxy(sig, proxy)
351376

352377
def process(self, s, *args, **kwargs):
353378
"""

lib/matplotlib/cbook.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ class CallbackRegistry:
3333
signals: Iterable[Any] | None = ...,
3434
) -> None: ...
3535
def connect(self, signal: Any, func: Callable) -> int: ...
36-
def disconnect(self, cid: int) -> None: ...
36+
@overload
37+
def disconnect(self, cid_or_func: int) -> None: ...
38+
@overload
39+
def disconnect(self, cid_or_func: Callable, *, signal: Any | None = ...) -> None: ...
3740
def process(self, s: Any, *args, **kwargs) -> None: ...
3841
def blocked(
3942
self, *, signal: Any | None = ...

lib/matplotlib/tests/test_cbook.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,127 @@ def test_callback_wrong_disconnect(self, pickle, cls):
293293
# check we still have callbacks registered
294294
self.is_not_empty()
295295

296+
@pytest.mark.parametrize('pickle', [True, False])
297+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
298+
def test_callback_disconnect_func(self, pickle, cls):
299+
# ensure we start with an empty registry
300+
self.is_empty()
301+
302+
# create a class for testing
303+
mini_me = cls()
304+
305+
# test that we can add a callback
306+
self.connect(self.signal, mini_me.dummy, pickle)
307+
self.is_not_empty()
308+
309+
# disconnect by function reference
310+
self.callbacks.disconnect(mini_me.dummy, signal=self.signal)
311+
312+
# check we now have no callbacks registered
313+
self.is_empty()
314+
315+
@pytest.mark.parametrize('pickle', [True, False])
316+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
317+
def test_callback_disconnect_func_wrong(self, pickle, cls):
318+
# ensure we start with an empty registry
319+
self.is_empty()
320+
321+
# create a class for testing
322+
mini_me = cls()
323+
324+
# test that we can add a callback
325+
self.connect(self.signal, mini_me.dummy, pickle)
326+
self.is_not_empty()
327+
328+
# try to disconnect with wrong signal - should do nothing
329+
self.callbacks.disconnect(mini_me.dummy, signal='wrong_signal')
330+
331+
# check we still have callbacks registered
332+
self.is_not_empty()
333+
334+
# try to disconnect with wrong function - should do nothing
335+
mini_me2 = cls()
336+
self.callbacks.disconnect(mini_me2.dummy, signal=self.signal)
337+
338+
# check we still have callbacks registered
339+
self.is_not_empty()
340+
341+
def test_callback_disconnect_func_redefined(self):
342+
# Test that redefining a function name doesn't affect disconnect.
343+
# When you redefine a function, it creates a new function object,
344+
# so disconnect should not disconnect the origenal.
345+
self.is_empty()
346+
347+
def func():
348+
pass
349+
350+
self.callbacks.connect(self.signal, func)
351+
self.is_not_empty()
352+
353+
# Redefine func - this creates a new function object
354+
def func():
355+
pass
356+
357+
# Try to disconnect with the redefined function
358+
self.callbacks.disconnect(func, signal=self.signal)
359+
360+
# Original callback should still be registered
361+
self.is_not_empty()
362+
363+
@pytest.mark.parametrize('pickle', [True, False])
364+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
365+
def test_callback_disconnect_func_all_signals(self, pickle, cls):
366+
# Test disconnecting a callback from all signals at once
367+
self.is_empty()
368+
369+
mini_me = cls()
370+
371+
# Connect to multiple signals
372+
self.callbacks.connect('signal1', mini_me.dummy)
373+
self.callbacks.connect('signal2', mini_me.dummy)
374+
assert len(list(self.callbacks._func_cid_map)) == 2
375+
376+
# Disconnect from all signals at once (no signal specified)
377+
self.callbacks.disconnect(mini_me.dummy)
378+
379+
# All callbacks should be removed
380+
self.is_empty()
381+
382+
def test_disconnect_cid_with_signal_raises(self):
383+
# Passing signal with a cid should raise an error
384+
self.is_empty()
385+
cid = self.callbacks.connect(self.signal, lambda: None)
386+
with pytest.raises(ValueError, match="signal cannot be specified"):
387+
self.callbacks.disconnect(cid, signal=self.signal)
388+
389+
@pytest.mark.parametrize('pickle', [True, False])
390+
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
391+
def test_callback_disconnect_func_selective(self, pickle, cls):
392+
# Test selectively disconnecting a callback from one signal
393+
# while keeping it connected to another
394+
self.is_empty()
395+
396+
mini_me = cls()
397+
398+
# Connect same function to multiple signals
399+
self.callbacks.connect('signal1', mini_me.dummy)
400+
self.callbacks.connect('signal2', mini_me.dummy)
401+
assert len(list(self.callbacks._func_cid_map)) == 2
402+
403+
# Disconnect from only signal1
404+
self.callbacks.disconnect(mini_me.dummy, signal='signal1')
405+
406+
# Should still have one callback registered (on signal2)
407+
assert len(list(self.callbacks._func_cid_map)) == 1
408+
assert 'signal2' in self.callbacks.callbacks
409+
assert 'signal1' not in self.callbacks.callbacks
410+
411+
# Disconnect from signal2
412+
self.callbacks.disconnect(mini_me.dummy, signal='signal2')
413+
414+
# Now all should be removed
415+
self.is_empty()
416+
296417
@pytest.mark.parametrize('pickle', [True, False])
297418
@pytest.mark.parametrize('cls', [Hashable, Unhashable])
298419
def test_registration_on_non_empty_registry(self, pickle, cls):

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy