Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Enhance object dictionary handling in type and object modules
- Modify set_dict method to allow assigning dict to objects without existing dict
- Update error handling in object_set_dict to provide more precise error message
- Refactor type module to correctly handle dictionary assignment
  • Loading branch information
key262yek committed Feb 14, 2025
commit b8d6f7b5947aa99a27d429c566749b761b0a2543
8 changes: 6 additions & 2 deletions vm/src/builtins/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,9 +496,13 @@ pub fn object_get_dict(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyDict
.ok_or_else(|| vm.new_attribute_error("This object has no __dict__".to_owned()))
}

pub fn object_set_dict(obj: PyObjectRef, dict: PySetterValue<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, PySetterValue::Delete, vm)?;
object::object_set_dict(obj, PySetterValue::Assign(value.try_into_value(vm)?), vm)?;
Copy link
Copy Markdown
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
Copy Markdown
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
12 changes: 9 additions & 3 deletions vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,21 +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: PySetterValue<PyDictRef>) -> Result<(), PyDictRef> {
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, PySetterValue::Assign(dict)) => 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) => Ok(()),
(None, PySetterValue::Delete) => Err(PySetterValue::Delete),
}
}

Expand Down