Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Implement del object.__dict__ #5509

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions vm/src/builtins/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,14 @@ pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDict
obj.dict()
.ok_or_else(|| vm.new_attribute_error("This object has no __dict__".to_owned()))
}
pub fn object_set_dict(obj: PyObjectRef, dict: PyDictRef, vm: &VirtualMachine) -> PyResult<()> {

pub fn object_set_dict(
obj: PyObjectRef,
dict: PySetterValue<PyDictRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
obj.set_dict(dict)
.map_err(|_| vm.new_attribute_error("This object has no __dict__".to_owned()))
.map_err(|_| vm.new_attribute_error("This object has no __dict__ to delete".to_owned()))
}

pub fn init(ctx: &Context) {
Expand Down
2 changes: 1 addition & 1 deletion vm/src/builtins/type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1288,7 +1288,7 @@ fn subtype_set_dict(obj: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -
descr_set(&descr, obj, PySetterValue::Assign(value), vm)
}
None => {
object::object_set_dict(obj, value.try_into_value(vm)?, vm)?;
object::object_set_dict(obj, PySetterValue::Assign(value.try_into_value(vm)?), vm)?;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Cannot resolve Lib/test/test_descr.py:3399 test_set_dict
    If I don't change code like this, object_set_dict fails to be compiled because now value must be the type PySetterValue<PyDictRef>.
    There are two option for it, pass the value PySetterValue::Assign or PySetterValue::Delete

For the case of Assign I get

ERROR: test_set_dict (__main__.ClassPropertiesAndMethods.test_set_dict)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Lib/test/test_descr.py", line 3415, in test_set_dict
    del a.__dict__ # Deleting __dict__ is allowed
TypeError: Expected type 'dict' but 'NoneType' found.

For Delete case,

ERROR: test_set_dict (__main__.ClassPropertiesAndMethods.test_set_dict)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "Lib/test/test_descr.py", line 3404, in test_set_dict
    self.assertEqual(a.b, 1)
AttributeError: 'C' object has no attribute 'b'

Test function

def test_set_dict(self):
        # Testing __dict__ assignment...
        class C(object): pass
        a = C()
        a.__dict__ = {'b': 1}
        self.assertEqual(a.b, 1)
        def cant(x, dict):
            try:
                x.__dict__ = dict
            except (AttributeError, TypeError):
                pass
            else:
                self.fail("shouldn't allow %r.__dict__ = %r" % (x, dict))
        cant(a, None)
        cant(a, [])
        cant(a, 1)
        del a.__dict__ # Deleting __dict__ is allowed

Copy link
Contributor Author

@key262yek key262yek Feb 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified problem is this.

If I try to delete __dict__, following methods are called.

  1. (getset.rs:164) set - PySetterValue::Delete becomes None
impl<F, T, V, R> IntoPySetterFunc<(OwnedParam<T>, V, R, VirtualMachine)> for F
where
    F: Fn(T, V, &VirtualMachine) -> R + 'static + Send + Sync,
    T: TryFromObject,
    V: FromPySetterValue,
    R: IntoPyNoResult,
{
    fn set(&self, obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
        let obj = T::try_from_object(vm, obj)?;
        let value = V::from_setter_value(vm, value)?;  // None = V::from_setter_value(vm, PySetterValue::Delete)?;
        (self)(obj, value, vm).into_noresult()  // subtype_set_dict(obj, None, vm)
    }
}

impl<T> FromPySetterValue for T
where
    T: Sized + TryFromObject,
{
    #[inline]
    fn from_setter_value(vm: &VirtualMachine, obj: PySetterValue) -> PyResult<Self> {
        match obj {
            PySetterValue::Assign(obj) => T::try_from_object(vm, obj),
            PySetterValue::Delete => T::try_from_object(vm, vm.ctx.none()),
        }
    }
}
  1. (type.rs:1275) subtype_set_dict - value should be PySetterValue again.
fn subtype_set_dict(obj: PyObjectRef, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
    let cls = obj.class();
    match find_base_dict_descr(cls, vm) {
        Some(descr) => {
            let descr_set = descr
                .class()
                .mro_find_map(|cls| cls.slots.descr_set.load())
                .ok_or_else(|| {
                    vm.new_type_error(format!(
                        "this __dict__ descriptor does not support '{}' objects",
                        cls.name()
                    ))
                })?;
            descr_set(&descr, obj, PySetterValue::Assign(value), vm)
        }
        None => {
            object::object_set_dict(obj, PySetterValue::Assign(value.try_into_value(vm)?), vm)?;
            Ok(())
        }
    }
}

Since the first V::from_setter_value(vm, value) give same result for value = PySetterValue::Assign(None) and value = PySetterValue::Delete to None, This kind of error occurs.

Ok(())
}
}
Expand Down
6 changes: 4 additions & 2 deletions vm/src/function/getset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ where
{
#[inline]
fn from_setter_value(vm: &VirtualMachine, obj: PySetterValue) -> PyResult<Self> {
let obj = obj.ok_or_else(|| vm.new_type_error("can't delete attribute".to_owned()))?;
T::try_from_object(vm, obj)
match obj {
PySetterValue::Assign(obj) => T::try_from_object(vm, obj),
PySetterValue::Delete => T::try_from_object(vm, vm.ctx.none()),
}
Comment on lines +39 to +42
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Affect multiple attribute in getset.rs
    By this code change, the test Lib/test/test_exceptions.py:620 testInvalidAttrs fails
def testInvalidAttrs(self):
        self.assertRaises(TypeError, setattr, Exception(), '__cause__', 1)
        self.assertRaises(TypeError, delattr, Exception(), '__cause__')
        self.assertRaises(TypeError, setattr, Exception(), '__context__', 1)
        self.assertRaises(TypeError, delattr, Exception(), '__context__')

This is because FromPySetterValue trait affect not only dict but also other attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found only one way to restrict deletion of attribute except __dict__ by editing following part

// vm/src/builtins/getset.rs: 97
     #[pyslot]
    fn descr_set(
        zelf: &PyObject,
        obj: PyObjectRef,
        value: PySetterValue<PyObjectRef>,
        vm: &VirtualMachine,
    ) -> PyResult<()> {
        let zelf = zelf.try_to_ref::<Self>(vm)?;
        if let Some(ref f) = zelf.setter {
            match value {
                PySetterValue::Assign(v) => f(vm, obj, PySetterValue::Assign(v)),
                PySetterValue::Delete if zelf.name == "__dict__" => {
                    f(vm, obj, PySetterValue::Delete)
                }
                _ => Err(vm.new_type_error("can't delete attribute".to_owned())),
            }
        } else {
            Err(vm.new_attribute_error(format!(
                "attribute '{}' of '{}' objects is not writable",
                zelf.name,
                obj.class().name()
            )))
        }
    }

}
}

Expand Down
23 changes: 19 additions & 4 deletions vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use crate::{
lock::{PyMutex, PyMutexGuard, PyRwLock},
refcount::RefCount,
},
function::PySetterValue,
vm::VirtualMachine,
};
use itertools::Itertools;
Expand Down Expand Up @@ -707,13 +708,27 @@ impl PyObject {

/// Set the dict field. Returns `Err(dict)` if this object does not have a dict field
/// in the first place.
pub fn set_dict(&self, dict: PyDictRef) -> Result<(), PyDictRef> {
match self.instance_dict() {
Some(d) => {
pub fn set_dict(&self, dict: PySetterValue<PyDictRef>) -> Result<(), PySetterValue<PyDictRef>> {
match (self.instance_dict(), dict) {
(Some(d), PySetterValue::Assign(dict)) => {
d.set(dict);
Ok(())
}
None => Err(dict),
(None, PySetterValue::Assign(dict)) => {
unsafe {
let ptr = self as *const _ as *mut PyObject;
(*ptr).0.dict = Some(InstanceDict::new(dict));
}
Ok(())
}
(Some(_), PySetterValue::Delete) => {
unsafe {
let ptr = self as *const _ as *mut PyObject;
(*ptr).0.dict = None;
}
Ok(())
}
(None, PySetterValue::Delete) => Err(PySetterValue::Delete),
}
}

Expand Down
Loading