From 5158128564f942fbb9400a6d2b47f2743cf255e9 Mon Sep 17 00:00:00 2001 From: xiehuanyi <74809554+xiehuanyi@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:47:11 +0300 Subject: [PATCH] Honor falsey callable callbacks in WeakFunctionProxy --- docs/changes.rst | 7 +++++ src/wrapt/weakrefs.py | 8 +++--- tests/core/test_weak_function_proxy.py | 37 ++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 9f50db19..bf325209 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -100,6 +100,13 @@ Version 2.5.0 **Bugs Fixed** +* A callable callback which evaluated as false, such as an initially empty + callable collection, was passed a weak reference instead of the + ``wrapt.WeakFunctionProxy`` when its wrapped function expired. Such a + callback could also be called twice when both a bound method and its + instance expired. These callbacks now receive the proxy and are called + only once, as with other callable callbacks. + * Deleting the ``__module__`` or ``__doc__`` attribute of a proxy object, using ``del proxy.__doc__`` for example, crashed the Python interpreter when the C extension was in use. The attribute setters, which CPython diff --git a/src/wrapt/weakrefs.py b/src/wrapt/weakrefs.py index 86728478..071456de 100644 --- a/src/wrapt/weakrefs.py +++ b/src/wrapt/weakrefs.py @@ -65,9 +65,11 @@ def __init__(self, wrapped, callback=None): # then call it. Note that we avoid using a nested function for # the callback here so as not to cause any odd reference cycles. - _callback = callback and functools.partial( - _weak_function_proxy_callback, proxy=self, callback=callback - ) + _callback = None + if callback is not None: + _callback = functools.partial( + _weak_function_proxy_callback, proxy=self, callback=callback + ) self._self_expired = False self._self_owner = None diff --git a/tests/core/test_weak_function_proxy.py b/tests/core/test_weak_function_proxy.py index 310189b8..e9dd82d4 100644 --- a/tests/core/test_weak_function_proxy.py +++ b/tests/core/test_weak_function_proxy.py @@ -144,6 +144,43 @@ def callback(proxy): self.assertEqual(len(result), 1) self.assertEqual(id(proxy), result[0]) + def test_falsey_callback_receives_proxy(self): + class Callback(list): + def __call__(self, proxy): + self.append(id(proxy)) + + def function(): + return 42 + + callback = Callback() + proxy = wrapt.WeakFunctionProxy(function, callback) + self.assertEqual(proxy(), 42) + del function + gc.collect() + self.assertEqual(callback, [id(proxy)]) + + @unittest.skipIf( + sys.version_info[:2] == (3, 13) and sysconfig.get_config_var("Py_GIL_DISABLED"), + "Free-threaded CPython 3.13 immortalizes classes after a thread starts", + ) + def test_falsey_callback_runs_once_when_method_and_instance_expire(self): + class Callback(list): + def __call__(self, proxy): + self.append(id(proxy)) + + class Class: + def method(self): + return 42 + + callback = Callback() + instance = Class() + proxy = wrapt.WeakFunctionProxy(instance.method, callback) + self.assertEqual(proxy(), 42) + del instance + del Class + gc.collect() + self.assertEqual(callback, [id(proxy)]) + def test_instancemethod_delete_instance(self): class Class: def function(self, a, b):