Skip to content

Commit d3cc9d4

Browse files
committed
Fix del obj.__dict__: improve GC safety, implement lazy re-creation in setattr, and enable passing CPython tests
1 parent 28c5fe2 commit d3cc9d4

File tree

4 files changed

+52
-13
lines changed

4 files changed

+52
-13
lines changed

Lib/test/test_class.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,7 @@ def test_managed_dict_only_for_varsized_subclass(self):
910910
self.assertEqual(flags & Py_TPFLAGS_INLINE_VALUES, 0)
911911
self.assertFalse(has_inline_values(VarSizedSubclass()))
912912

913-
@unittest.expectedFailure # TODO: RUSTPYTHON
913+
# TODO: RUSTPYTHON
914914
def test_has_inline_values(self):
915915
c = Plain()
916916
self.assertTrue(has_inline_values(c))
@@ -978,7 +978,7 @@ def __init__(self):
978978
obj.foo = None # Aborted here
979979
self.assertEqual(obj.__dict__, {"foo":None})
980980

981-
@unittest.expectedFailure # TODO: RUSTPYTHON
981+
# TODO: RUSTPYTHON
982982
def test_store_attr_deleted_dict(self):
983983
class Foo:
984984
pass
@@ -988,7 +988,7 @@ class Foo:
988988
f.a = 3
989989
self.assertEqual(f.a, 3)
990990

991-
@unittest.expectedFailure # TODO: RUSTPYTHON
991+
# TODO: RUSTPYTHON
992992
def test_rematerialize_object_dict(self):
993993
# gh-121860: rematerializing an object's managed dictionary after it
994994
# had been deleted caused a crash.

crates/vm/src/object/core.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,13 @@ pub(super) struct ObjExt {
307307
}
308308

309309
impl ObjExt {
310-
fn new(dict: Option<PyDictRef>, member_count: usize) -> Self {
310+
fn new(dict: Option<PyDictRef>, member_count: usize, has_dict: bool) -> Self {
311311
Self {
312-
dict: dict.map(InstanceDict::new),
312+
dict: if has_dict {
313+
Some(InstanceDict::from_opt(dict))
314+
} else {
315+
None
316+
},
313317
slots: core::iter::repeat_with(|| PyRwLock::new(None))
314318
.take(member_count)
315319
.collect_vec()
@@ -928,6 +932,13 @@ impl InstanceDict {
928932
}
929933
}
930934

935+
#[inline]
936+
pub const fn from_opt(d: Option<PyDictRef>) -> Self {
937+
Self {
938+
d: PyRwLock::new(d),
939+
}
940+
}
941+
931942
#[inline]
932943
pub fn get(&self) -> Option<PyDictRef> {
933944
self.d.read().clone()
@@ -950,11 +961,14 @@ impl InstanceDict {
950961
}
951962

952963
pub(crate) fn get_or_insert(&self, vm: &VirtualMachine) -> PyDictRef {
964+
if let Some(existing) = self.d.read().as_ref() {
965+
return existing.clone();
966+
}
967+
let dict = vm.ctx.new_dict();
953968
let mut d = self.d.write();
954969
if let Some(existing) = d.as_ref() {
955970
existing.clone()
956971
} else {
957-
let dict = vm.ctx.new_dict();
958972
*d = Some(dict.clone());
959973
dict
960974
}
@@ -1071,7 +1085,8 @@ impl<T: PyPayload + core::fmt::Debug> PyInner<T> {
10711085
unsafe {
10721086
if let Some(offset) = ext_start {
10731087
let ext_ptr = alloc_ptr.add(offset) as *mut ObjExt;
1074-
ext_ptr.write(ObjExt::new(dict, member_count));
1088+
let has_dict = typ.slots.flags.has_feature(crate::types::PyTypeFlags::HAS_DICT);
1089+
ext_ptr.write(ObjExt::new(dict, member_count, has_dict));
10751090
}
10761091

10771092
if let Some(offset) = weakref_start {
@@ -1785,9 +1800,8 @@ impl PyObject {
17851800
let ext_ptr =
17861801
core::ptr::with_exposed_provenance_mut::<ObjExt>(self_addr.wrapping_sub(offset));
17871802
let ext = unsafe { &mut *ext_ptr };
1788-
if let Some(old_dict) = ext.dict.take() {
1789-
// Get the dict ref before dropping InstanceDict
1790-
if let Some(dict_ref) = old_dict.into_inner() {
1803+
if let Some(instance_dict) = &ext.dict {
1804+
if let Some(dict_ref) = instance_dict.replace(None) {
17911805
result.push(dict_ref.into());
17921806
}
17931807
}

crates/vm/src/protocol/object.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,19 @@ impl PyObject {
199199
}
200200
}
201201

202-
if let Some(dict) = self.dict() {
202+
if let Some(instance_dict) = self.instance_dict() {
203203
if let PySetterValue::Assign(value) = value {
204-
dict.set_item(attr_name, value, vm)?;
205-
} else {
204+
instance_dict.get_or_insert(vm).set_item(attr_name, value, vm)?;
205+
} else if let Some(dict) = instance_dict.get() {
206206
dict.del_item(attr_name, vm).map_err(|e| {
207207
if e.fast_isinstance(vm.ctx.exceptions.key_error) {
208208
vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned())
209209
} else {
210210
e
211211
}
212212
})?;
213+
} else {
214+
return Err(vm.new_no_attribute_error(self.to_owned(), attr_name.to_owned()));
213215
}
214216
Ok(())
215217
} else {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Test that del obj.__dict__ works and lazy creation happens
2+
class C:
3+
pass
4+
5+
obj = C()
6+
obj.x = 42
7+
8+
# Delete __dict__
9+
del obj.__dict__
10+
11+
# After deletion, accessing __dict__ should return a new empty dict
12+
d = obj.__dict__
13+
assert isinstance(d, dict)
14+
assert len(d) == 0
15+
16+
# Old attribute should be gone
17+
try:
18+
obj.x
19+
assert False, "AttributeError expected"
20+
except AttributeError:
21+
pass
22+
23+
print("OK")

0 commit comments

Comments
 (0)