diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index cd0bdacaa..2e8e679c8 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -993,7 +993,6 @@ class ReferencesTestCase(TestBase): del root gc.collect() - @unittest.expectedFailure # TODO: RUSTPYTHON def test_callback_attribute(self): x = Object(1) callback = lambda ref: None @@ -1003,7 +1002,6 @@ class ReferencesTestCase(TestBase): ref2 = weakref.ref(x) self.assertIsNone(ref2.__callback__) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_callback_attribute_after_deletion(self): x = Object(1) ref = weakref.ref(x, self.callback) diff --git a/crates/vm/src/builtins/weakref.rs b/crates/vm/src/builtins/weakref.rs index e1b954525..0527142bb 100644 --- a/crates/vm/src/builtins/weakref.rs +++ b/crates/vm/src/builtins/weakref.rs @@ -85,6 +85,11 @@ impl Initializer for PyWeak { flags(BASETYPE) )] impl PyWeak { + #[pygetset] + fn __callback__(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.unwrap_or_none(self.get_callback()) + } + #[pyclassmethod] fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias { PyGenericAlias::from_args(cls, args, vm) diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index a8d7c09da..cf49cc42a 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -852,6 +852,29 @@ impl PyWeak { self.wr_object.load(Ordering::Acquire).is_null() } + /// Get the callback associated with this weak reference. + /// Returns `None` if there is no callback or if the referent has been + /// collected (at which point the callback was already consumed). + pub(crate) fn get_callback(&self) -> Option { + let obj_ptr = self.wr_object.load(Ordering::Acquire); + if obj_ptr.is_null() { + // Dead weakref: callback was consumed during clear + return None; + } + + let _lock = weakref_lock::lock(obj_ptr as usize); + + // Double-check under lock (clear may have run between our check and lock) + let obj_ptr = self.wr_object.load(Ordering::Relaxed); + if obj_ptr.is_null() { + return None; + } + + // Safety: we hold the stripe lock that protects the callback field + let callback = unsafe { &*self.callback.get() }; + callback.clone() + } + /// weakref_dealloc: remove from list if still linked. fn drop_inner(&self) { let obj_ptr = self.wr_object.load(Ordering::Acquire); diff --git a/extra_tests/snippets/stdlib_weakref.py b/extra_tests/snippets/stdlib_weakref.py index 17762addc..05d72275e 100644 --- a/extra_tests/snippets/stdlib_weakref.py +++ b/extra_tests/snippets/stdlib_weakref.py @@ -13,6 +13,30 @@ b = ref(a) assert callable(b) assert b() is a +# Test __callback__ property +assert b.__callback__ is None, ( + "weakref without callback should have __callback__ == None" +) + +callback = lambda r: None +c = ref(a, callback) +assert c.__callback__ is callback, "weakref with callback should return the callback" + +# Test __callback__ is read-only +try: + c.__callback__ = lambda r: None + assert False, "Setting __callback__ should raise AttributeError" +except AttributeError: + pass + +# Test __callback__ after referent deletion +x = X() +cb = lambda r: None +w = ref(x, cb) +assert w.__callback__ is cb +del x +assert w.__callback__ is None, "__callback__ should be None after referent is collected" + class G: def __init__(self, h):