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>
This commit is contained in:
Copilotandyouknowone authored and GitHub committed 2026-04-13 23:58:58 +09:00
1 parent 030da76d4e
commit 76755524df
4 files changed
+52 -2

No files matched your search

-2
View File
@@ -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)
+5
View File
@@ -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)
+23
View File
@@ -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<PyObjectRef> {
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);
+24
View File
@@ -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):