From 76755524df78d6b391444ce39fea318b7660c8dc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:58:58 +0900 Subject: [PATCH] Add `__callback__` property to weakref type (#7590) * Initial plan * Add __callback__ property to PyWeak for weakref compatibility Add get_callback() method to PyWeak in core.rs that safely reads the callback field under the stripe lock. Expose it as a read-only __callback__ property on the weakref type, matching CPython behavior: - Returns the callback function when one was provided and referent is alive - Returns None when no callback was provided - Returns None after the referent has been collected - Raises AttributeError on assignment (read-only) Remove @unittest.expectedFailure from test_callback_attribute and test_callback_attribute_after_deletion in test_weakref.py. Add regression tests to extra_tests/snippets/stdlib_weakref.py. Agent-Logs-Url: https://github.com/RustPython/RustPython/sessions/a8689daa-4476-4645-a935-0e13c7f7bb42 Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> * Fix ruff formatting in stdlib_weakref.py snippet Agent-Logs-Url: https://github.com/RustPython/RustPython/sessions/4995198f-e083-4dac-823a-166fcf54adc4 Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- Lib/test/test_weakref.py | 2 -- crates/vm/src/builtins/weakref.rs | 5 +++++ crates/vm/src/object/core.rs | 23 +++++++++++++++++++++++ extra_tests/snippets/stdlib_weakref.py | 24 ++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) 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):