Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions src/wrapt/weakrefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions tests/core/test_weak_function_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading