Skip to content

Commit 7e637e8

Browse files
Copilotyouknowone
andauthored
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>
1 parent 7e5e026 commit 7e637e8

File tree

4 files changed

+52
-2
lines changed

4 files changed

+52
-2
lines changed

Lib/test/test_weakref.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,6 @@ def cb(wparent):
993993
del root
994994
gc.collect()
995995

996-
@unittest.expectedFailure # TODO: RUSTPYTHON
997996
def test_callback_attribute(self):
998997
x = Object(1)
999998
callback = lambda ref: None
@@ -1003,7 +1002,6 @@ def test_callback_attribute(self):
10031002
ref2 = weakref.ref(x)
10041003
self.assertIsNone(ref2.__callback__)
10051004

1006-
@unittest.expectedFailure # TODO: RUSTPYTHON
10071005
def test_callback_attribute_after_deletion(self):
10081006
x = Object(1)
10091007
ref = weakref.ref(x, self.callback)

crates/vm/src/builtins/weakref.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,11 @@ impl Initializer for PyWeak {
8585
flags(BASETYPE)
8686
)]
8787
impl PyWeak {
88+
#[pygetset]
89+
fn __callback__(&self, vm: &VirtualMachine) -> PyObjectRef {
90+
vm.unwrap_or_none(self.get_callback())
91+
}
92+
8893
#[pyclassmethod]
8994
fn __class_getitem__(cls: PyTypeRef, args: PyObjectRef, vm: &VirtualMachine) -> PyGenericAlias {
9095
PyGenericAlias::from_args(cls, args, vm)

crates/vm/src/object/core.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,29 @@ impl PyWeak {
852852
self.wr_object.load(Ordering::Acquire).is_null()
853853
}
854854

855+
/// Get the callback associated with this weak reference.
856+
/// Returns `None` if there is no callback or if the referent has been
857+
/// collected (at which point the callback was already consumed).
858+
pub(crate) fn get_callback(&self) -> Option<PyObjectRef> {
859+
let obj_ptr = self.wr_object.load(Ordering::Acquire);
860+
if obj_ptr.is_null() {
861+
// Dead weakref: callback was consumed during clear
862+
return None;
863+
}
864+
865+
let _lock = weakref_lock::lock(obj_ptr as usize);
866+
867+
// Double-check under lock (clear may have run between our check and lock)
868+
let obj_ptr = self.wr_object.load(Ordering::Relaxed);
869+
if obj_ptr.is_null() {
870+
return None;
871+
}
872+
873+
// Safety: we hold the stripe lock that protects the callback field
874+
let callback = unsafe { &*self.callback.get() };
875+
callback.clone()
876+
}
877+
855878
/// weakref_dealloc: remove from list if still linked.
856879
fn drop_inner(&self) {
857880
let obj_ptr = self.wr_object.load(Ordering::Acquire);

extra_tests/snippets/stdlib_weakref.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,30 @@ class X:
1313
assert callable(b)
1414
assert b() is a
1515

16+
# Test __callback__ property
17+
assert b.__callback__ is None, (
18+
"weakref without callback should have __callback__ == None"
19+
)
20+
21+
callback = lambda r: None
22+
c = ref(a, callback)
23+
assert c.__callback__ is callback, "weakref with callback should return the callback"
24+
25+
# Test __callback__ is read-only
26+
try:
27+
c.__callback__ = lambda r: None
28+
assert False, "Setting __callback__ should raise AttributeError"
29+
except AttributeError:
30+
pass
31+
32+
# Test __callback__ after referent deletion
33+
x = X()
34+
cb = lambda r: None
35+
w = ref(x, cb)
36+
assert w.__callback__ is cb
37+
del x
38+
assert w.__callback__ is None, "__callback__ should be None after referent is collected"
39+
1640

1741
class G:
1842
def __init__(self, h):

0 commit comments

Comments
 (0)