From 4e42bd077c371a53bf987c55fa1a1393b22124d1 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 16 Mar 2019 17:31:01 +1300 Subject: [PATCH 1/7] Implement __setattr__ based on @nhynes(#540) PR --- vm/src/builtins.rs | 17 ++++++++--------- vm/src/frame.rs | 2 +- vm/src/obj/objobject.rs | 13 ++++++++++++- vm/src/vm.rs | 9 +++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/vm/src/builtins.rs b/vm/src/builtins.rs index 85ce77af0..e225773d6 100644 --- a/vm/src/builtins.rs +++ b/vm/src/builtins.rs @@ -658,15 +658,14 @@ fn builtin_round(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { } } -fn builtin_setattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { - arg_check!( - vm, - args, - required = [(obj, None), (attr, Some(vm.ctx.str_type())), (value, None)] - ); - let name = objstr::get_value(attr); - vm.ctx.set_attr(obj, &name, value.clone()); - Ok(vm.get_none()) +fn builtin_setattr( + obj: PyObjectRef, + attr: PyStringRef, + value: PyObjectRef, + vm: &mut VirtualMachine, +) -> PyResult<()> { + vm.set_attr(&obj, attr.into_object(), value)?; + Ok(()) } // builtin_slice diff --git a/vm/src/frame.rs b/vm/src/frame.rs index 4482e55de..6f8653f88 100644 --- a/vm/src/frame.rs +++ b/vm/src/frame.rs @@ -1146,7 +1146,7 @@ impl Frame { fn store_attr(&self, vm: &mut VirtualMachine, attr_name: &str) -> FrameResult { let parent = self.pop_value(); let value = self.pop_value(); - vm.ctx.set_attr(&parent, attr_name, value); + vm.set_attr(&parent, vm.new_str(attr_name.to_string()), value)?; Ok(None) } diff --git a/vm/src/obj/objobject.rs b/vm/src/obj/objobject.rs index 7615f4bb1..e4d979db6 100644 --- a/vm/src/obj/objobject.rs +++ b/vm/src/obj/objobject.rs @@ -93,7 +93,17 @@ fn object_hash(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { Err(vm.new_type_error("unhashable type".to_string())) } -// TODO: is object the right place for delattr? +fn object_setattr( + obj: PyInstanceRef, + name: PyStringRef, + value: PyObjectRef, + vm: &mut VirtualMachine, +) -> PyResult { + trace!("object.__setattr__({:?}, {}, {:?})", obj, name, value); + vm.ctx.set_attr(obj.as_object(), &name.value, value); + Ok(vm.get_none()) +} + fn object_delattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { arg_check!( vm, @@ -167,6 +177,7 @@ pub fn init(context: &PyContext) { context.set_attr(&object, "__le__", context.new_rustfunc(object_le)); context.set_attr(&object, "__gt__", context.new_rustfunc(object_gt)); context.set_attr(&object, "__ge__", context.new_rustfunc(object_ge)); + context.set_attr(&object, "__setattr__", context.new_rustfunc(object_setattr)); context.set_attr(&object, "__delattr__", context.new_rustfunc(object_delattr)); context.set_attr(&object, "__dict__", context.new_property(object_dict)); context.set_attr(&object, "__dir__", context.new_rustfunc(object_dir)); diff --git a/vm/src/vm.rs b/vm/src/vm.rs index 3b3799d0f..593c15ea9 100644 --- a/vm/src/vm.rs +++ b/vm/src/vm.rs @@ -547,6 +547,15 @@ impl VirtualMachine { self.call_method(&obj, "__getattribute__", vec![attr_name]) } + pub fn set_attr( + &mut self, + obj: &PyObjectRef, + attr_name: PyObjectRef, + attr_value: PyObjectRef, + ) -> PyResult { + self.call_method(&obj, "__setattr__", vec![attr_name, attr_value]) + } + pub fn del_attr(&mut self, obj: &PyObjectRef, attr_name: PyObjectRef) -> PyResult { self.call_method(&obj, "__delattr__", vec![attr_name]) } From 63d40edbc644930c3bcac4a2df930d04b8e893d5 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 16 Mar 2019 20:49:29 +1300 Subject: [PATCH 2/7] Added bunch af attribute has/get/set/delete tests and fixed getattr and hasattr handling of exceptions. --- tests/snippets/attr.py | 84 ++++++++++++++++++++++++++++++++++++++++++ vm/src/builtins.rs | 46 +++++++++++++---------- 2 files changed, 111 insertions(+), 19 deletions(-) create mode 100644 tests/snippets/attr.py diff --git a/tests/snippets/attr.py b/tests/snippets/attr.py new file mode 100644 index 000000000..439acfa79 --- /dev/null +++ b/tests/snippets/attr.py @@ -0,0 +1,84 @@ +from testutils import assertRaises + + +class A: + pass + + +a = A() +a.b = 10 +assert hasattr(a, 'b') +assert a.b == 10 + +# test override attribute +setattr(a, 'b', 12) +assert a.b == 12 +assert getattr(a, 'b') == 12 + +# test non-existent attribute +with assertRaises(AttributeError): + _ = a.c + +with assertRaises(AttributeError): + getattr(a, 'c') + +assert getattr(a, 'c', 21) == 21 + +# test set attribute +setattr(a, 'c', 20) +assert hasattr(a, 'c') +assert a.c == 20 + +# test delete attribute +delattr(a, 'c') +assert not hasattr(a, 'c') +with assertRaises(AttributeError): + _ = a.c + +# test setting attribute on builtin +with assertRaises(AttributeError): + None.a = 1 + +with assertRaises(AttributeError): + setattr(None, 'a', 2) + + +attrs = {} + + +class CustomLookup: + def __getattr__(self, item): + return "value_{}".format(item) + + def __setattr__(self, key, value): + attrs[key] = value + + +custom = CustomLookup() + +assert custom.attr == "value_attr" + +custom.a = 2 +custom.b = 5 +assert attrs == {'a': 2, 'b': 5} + + +class GetRaise: + def __init__(self, ex): + self.ex = ex + + def __getattr__(self, item): + raise self.ex + + +assert not hasattr(GetRaise(AttributeError()), 'a') +with assertRaises(AttributeError): + getattr(GetRaise(AttributeError()), 'a') +assert getattr(GetRaise(AttributeError()), 'a', 11) == 11 + +with assertRaises(KeyError): + hasattr(GetRaise(KeyError()), 'a') +with assertRaises(KeyError): + getattr(GetRaise(KeyError()), 'a') +with assertRaises(KeyError): + getattr(GetRaise(KeyError()), 'a', 11) diff --git a/vm/src/builtins.rs b/vm/src/builtins.rs index e225773d6..dccaeee84 100644 --- a/vm/src/builtins.rs +++ b/vm/src/builtins.rs @@ -18,7 +18,7 @@ use crate::obj::objstr::{self, PyStringRef}; use crate::obj::objtype; use crate::frame::Scope; -use crate::function::{Args, PyFuncArgs}; +use crate::function::{Args, OptionalArg, PyFuncArgs}; use crate::pyobject::{ AttributeProtocol, IdProtocol, PyContext, PyObjectRef, PyResult, TypeProtocol, }; @@ -302,30 +302,38 @@ fn builtin_format(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { vm.call_method(obj, "__format__", vec![format_spec]) } -fn builtin_getattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { - arg_check!( - vm, - args, - required = [(obj, None), (attr, Some(vm.ctx.str_type()))] - ); - vm.get_attribute(obj.clone(), attr.clone()) +fn catch_attr_exception(ex: PyObjectRef, default: T, vm: &mut VirtualMachine) -> PyResult { + if objtype::isinstance(&ex, &vm.ctx.exceptions.attribute_error) { + Ok(default) + } else { + Err(ex) + } +} + +fn builtin_getattr( + obj: PyObjectRef, + attr: PyStringRef, + default: OptionalArg, + vm: &mut VirtualMachine, +) -> PyResult { + let ret = vm.get_attribute(obj.clone(), attr.into_object()); + if let OptionalArg::Present(default) = default { + ret.or_else(|ex| catch_attr_exception(ex, default, vm)) + } else { + ret + } } fn builtin_globals(vm: &mut VirtualMachine, _args: PyFuncArgs) -> PyResult { Ok(vm.current_scope().globals.clone()) } -fn builtin_hasattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { - arg_check!( - vm, - args, - required = [(obj, None), (attr, Some(vm.ctx.str_type()))] - ); - let has_attr = match vm.get_attribute(obj.clone(), attr.clone()) { - Ok(..) => true, - Err(..) => false, - }; - Ok(vm.context().new_bool(has_attr)) +fn builtin_hasattr(obj: PyObjectRef, attr: PyStringRef, vm: &mut VirtualMachine) -> PyResult { + if let Err(ex) = vm.get_attribute(obj.clone(), attr.into_object()) { + catch_attr_exception(ex, false, vm) + } else { + Ok(true) + } } fn builtin_hash(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { From c77b1f3cf5adee9286d70c969115e9b7d5a3c4db Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 17 Mar 2019 11:31:31 +1300 Subject: [PATCH 3/7] Fix bug with __getattr__ --- tests/snippets/attr.py | 14 ++++++++------ tests/snippets/testutils.py | 2 +- vm/src/obj/objobject.rs | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/snippets/attr.py b/tests/snippets/attr.py index 439acfa79..9ee0b071c 100644 --- a/tests/snippets/attr.py +++ b/tests/snippets/attr.py @@ -35,12 +35,13 @@ assert not hasattr(a, 'c') with assertRaises(AttributeError): _ = a.c +# TODO Fix # test setting attribute on builtin -with assertRaises(AttributeError): - None.a = 1 - -with assertRaises(AttributeError): - setattr(None, 'a', 2) +# with assertRaises(AttributeError): +# None.a = 1 +# +# with assertRaises(AttributeError): +# setattr(None, 'a', 2) attrs = {} @@ -60,7 +61,8 @@ assert custom.attr == "value_attr" custom.a = 2 custom.b = 5 -assert attrs == {'a': 2, 'b': 5} +assert attrs['a'] == 2 +assert attrs['b'] == 5 class GetRaise: diff --git a/tests/snippets/testutils.py b/tests/snippets/testutils.py index 8237ceb62..7dda57b17 100644 --- a/tests/snippets/testutils.py +++ b/tests/snippets/testutils.py @@ -29,7 +29,7 @@ class assertRaises: def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: - failmsg = '{!s} was not raised'.format(self.expected.__name_) + failmsg = '{!s} was not raised'.format(self.expected.__name__) assert False, failmsg if not issubclass(exc_type, self.expected): return False diff --git a/vm/src/obj/objobject.rs b/vm/src/obj/objobject.rs index e4d979db6..56dc80b56 100644 --- a/vm/src/obj/objobject.rs +++ b/vm/src/obj/objobject.rs @@ -249,7 +249,7 @@ fn object_getattribute(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { } else if let Some(attr) = cls.get_attr(&name) { vm.call_get_descriptor(attr, obj.clone()) } else if let Some(getter) = cls.get_attr("__getattr__") { - vm.invoke(getter, vec![cls, name_str.clone()]) + vm.invoke(getter, vec![obj.clone(), name_str.clone()]) } else { Err(vm.new_attribute_error(format!("{} has no attribute '{}'", obj, name))) } From 48dd507cad1503e2c55ba0d27d5668ebce8e2a50 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 17 Mar 2019 12:22:31 +1300 Subject: [PATCH 4/7] Remove dict from object --- tests/snippets/attr.py | 13 ++++++------- vm/src/obj/objobject.rs | 31 +++++++++++++++++++++---------- vm/src/pyobject.rs | 9 +++++++++ 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/tests/snippets/attr.py b/tests/snippets/attr.py index 9ee0b071c..cf7162cde 100644 --- a/tests/snippets/attr.py +++ b/tests/snippets/attr.py @@ -35,14 +35,13 @@ assert not hasattr(a, 'c') with assertRaises(AttributeError): _ = a.c -# TODO Fix -# test setting attribute on builtin -# with assertRaises(AttributeError): -# None.a = 1 -# -# with assertRaises(AttributeError): -# setattr(None, 'a', 2) +# test setting attribute on builtin +with assertRaises(AttributeError): + object().a = 1 + +with assertRaises(AttributeError): + setattr(object(), 'a', 2) attrs = {} diff --git a/vm/src/obj/objobject.rs b/vm/src/obj/objobject.rs index 56dc80b56..43addcbc4 100644 --- a/vm/src/obj/objobject.rs +++ b/vm/src/obj/objobject.rs @@ -4,8 +4,8 @@ use super::objtype; use crate::function::PyFuncArgs; use crate::obj::objproperty::PropertyBuilder; use crate::pyobject::{ - AttributeProtocol, DictProtocol, IdProtocol, PyAttributes, PyContext, PyObjectRef, PyRef, - PyResult, PyValue, TypeProtocol, + AttributeProtocol, DictProtocol, IdProtocol, PyAttributes, PyContext, PyObject, PyObjectRef, + PyRef, PyResult, PyValue, TypeProtocol, }; use crate::vm::VirtualMachine; @@ -22,9 +22,12 @@ pub type PyInstanceRef = PyRef; pub fn new_instance(vm: &mut VirtualMachine, mut args: PyFuncArgs) -> PyResult { // more or less __new__ operator - let type_ref = args.shift(); - let obj = vm.ctx.new_instance(type_ref.clone(), None); - Ok(obj) + let cls = args.shift(); + Ok(if cls.is(&vm.ctx.object) { + PyObject::new_without_dict(PyInstance, cls) + } else { + PyObject::new(PyInstance, cls) + }) } fn object_eq(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { @@ -95,13 +98,21 @@ fn object_hash(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { fn object_setattr( obj: PyInstanceRef, - name: PyStringRef, + attr_name: PyStringRef, value: PyObjectRef, vm: &mut VirtualMachine, -) -> PyResult { - trace!("object.__setattr__({:?}, {}, {:?})", obj, name, value); - vm.ctx.set_attr(obj.as_object(), &name.value, value); - Ok(vm.get_none()) +) -> PyResult<()> { + trace!("object.__setattr__({:?}, {}, {:?})", obj, attr_name, value); + if let Some(ref dict) = obj.as_object().dict { + dict.borrow_mut().insert(attr_name.value.clone(), value); + Ok(()) + } else { + let type_name = objtype::get_type_name(&obj.as_object().typ()); + Err(vm.new_attribute_error(format!( + "'{}' object has no attribute '{}'", + type_name, &attr_name.value + ))) + } } fn object_delattr(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { diff --git a/vm/src/pyobject.rs b/vm/src/pyobject.rs index afcf41c89..ef9f51aa9 100644 --- a/vm/src/pyobject.rs +++ b/vm/src/pyobject.rs @@ -1112,6 +1112,15 @@ impl PyObject { .into_ref() } + pub fn new_without_dict(payload: T, typ: PyObjectRef) -> PyObjectRef { + PyObject { + typ, + dict: None, + payload: Box::new(payload), + } + .into_ref() + } + // Move this object into a reference object, transferring ownership. pub fn into_ref(self) -> PyObjectRef { Rc::new(self) From 6c3a402eca7e70eae95299b3468c5906fb343113 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 17 Mar 2019 12:38:12 +1300 Subject: [PATCH 5/7] Fix setting attributes on modules --- vm/src/obj/objmodule.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vm/src/obj/objmodule.rs b/vm/src/obj/objmodule.rs index 6ef1cd012..da7d28248 100644 --- a/vm/src/obj/objmodule.rs +++ b/vm/src/obj/objmodule.rs @@ -1,3 +1,4 @@ +use crate::obj::objstr::PyStringRef; use crate::pyobject::{DictProtocol, PyContext, PyObjectRef, PyRef, PyResult, PyValue}; use crate::vm::VirtualMachine; @@ -24,10 +25,15 @@ impl PyModuleRef { .collect(); Ok(vm.ctx.new_list(keys)) } + + fn set_attr(self, attr: PyStringRef, value: PyObjectRef, vm: &mut VirtualMachine) { + self.dict.set_item(&vm.ctx, &attr.value, value) + } } pub fn init(context: &PyContext) { extend_class!(&context, &context.module_type, { - "__dir__" => context.new_rustfunc(PyModuleRef::dir) + "__dir__" => context.new_rustfunc(PyModuleRef::dir), + "__setattr__" => context.new_rustfunc(PyModuleRef::set_attr) }); } From a5050ebafec9812d0a8d871fd10cdb24b36e3b54 Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 17 Mar 2019 14:49:32 +1300 Subject: [PATCH 6/7] Make property return itself if invoke by class binding --- tests/snippets/property.py | 4 ++++ vm/src/obj/objproperty.rs | 14 +++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/snippets/property.py b/tests/snippets/property.py index 50f8e8fc8..2965224e6 100644 --- a/tests/snippets/property.py +++ b/tests/snippets/property.py @@ -16,6 +16,8 @@ f = Fubar() assert f.foo == 100 assert f.foo == 101 +assert type(Fubar.foo) is property + null_property = property() assert type(null_property) is property @@ -45,3 +47,5 @@ assert p1.deleter(3).fdel == 3 assert p1.getter(None).fget == "a" assert p1.setter(None).fset == "b" assert p1.deleter(None).fdel == "c" + +assert p1.__get__(None, object) is p1 diff --git a/vm/src/obj/objproperty.rs b/vm/src/obj/objproperty.rs index e403bb53a..060bb07fa 100644 --- a/vm/src/obj/objproperty.rs +++ b/vm/src/obj/objproperty.rs @@ -6,7 +6,7 @@ use crate::function::IntoPyNativeFunc; use crate::function::OptionalArg; use crate::obj::objstr::PyStringRef; use crate::obj::objtype::PyClassRef; -use crate::pyobject::{PyContext, PyObject, PyObjectRef, PyRef, PyResult, PyValue}; +use crate::pyobject::{IdProtocol, PyContext, PyObject, PyObjectRef, PyRef, PyResult, PyValue}; use crate::vm::VirtualMachine; /// Read-only property, doesn't have __set__ or __delete__ @@ -25,7 +25,11 @@ pub type PyReadOnlyPropertyRef = PyRef; impl PyReadOnlyPropertyRef { fn get(self, obj: PyObjectRef, _owner: PyClassRef, vm: &mut VirtualMachine) -> PyResult { - vm.invoke(self.getter.clone(), obj) + if obj.is(&vm.ctx.none) { + Ok(self.into_object()) + } else { + vm.invoke(self.getter.clone(), obj) + } } } @@ -66,7 +70,11 @@ impl PyPropertyRef { fn get(self, obj: PyObjectRef, _owner: PyClassRef, vm: &mut VirtualMachine) -> PyResult { if let Some(getter) = self.getter.as_ref() { - vm.invoke(getter.clone(), obj) + if obj.is(&vm.ctx.none) { + Ok(self.into_object()) + } else { + vm.invoke(getter.clone(), obj) + } } else { Err(vm.new_attribute_error("unreadable attribute".to_string())) } From 08e66b5002f3a3a30db72b350b18c61d367ef1fc Mon Sep 17 00:00:00 2001 From: ben Date: Sun, 17 Mar 2019 19:53:16 +1300 Subject: [PATCH 7/7] Create workaround for properties on None --- vm/src/obj/objnone.rs | 57 ++++++++++++++++++++++++++++++++++++++- vm/src/obj/objproperty.rs | 13 +++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/vm/src/obj/objnone.rs b/vm/src/obj/objnone.rs index fe63ecd98..1c0051219 100644 --- a/vm/src/obj/objnone.rs +++ b/vm/src/obj/objnone.rs @@ -1,6 +1,9 @@ use crate::function::PyFuncArgs; +use crate::obj::objproperty::PyPropertyRef; +use crate::obj::objstr::PyStringRef; use crate::pyobject::{ - IntoPyObject, PyContext, PyObjectRef, PyRef, PyResult, PyValue, TypeProtocol, + AttributeProtocol, IntoPyObject, PyContext, PyObjectRef, PyRef, PyResult, PyValue, + TryFromObject, TypeProtocol, }; use crate::vm::VirtualMachine; @@ -39,6 +42,57 @@ impl PyNoneRef { fn bool(self, _vm: &mut VirtualMachine) -> PyResult { Ok(false) } + + fn get_attribute(self, name: PyStringRef, vm: &mut VirtualMachine) -> PyResult { + trace!("None.__getattribute__({:?}, {:?})", self, name); + let cls = self.typ().into_object(); + + // Properties use a comparision with None to determine if they are either invoked by am + // instance binding or a class binding. But if the object itself is None then this detection + // won't work. Instead we call a special function on property that bypasses this check, as + // we are invoking it as a instance binding. + // + // In CPython they instead call the slot tp_descr_get with NULL to indicates it's an + // instance binding. + // https://github.com/python/cpython/blob/master/Objects/typeobject.c#L3281 + fn call_descriptor( + descriptor: PyObjectRef, + get_func: PyObjectRef, + obj: PyObjectRef, + cls: PyObjectRef, + vm: &mut VirtualMachine, + ) -> PyResult { + if let Ok(property) = PyPropertyRef::try_from_object(vm, descriptor.clone()) { + property.instance_binding_get(obj, vm) + } else { + vm.invoke(get_func, vec![descriptor, obj, cls]) + } + } + + if let Some(attr) = cls.get_attr(&name.value) { + let attr_class = attr.typ(); + if attr_class.has_attr("__set__") { + if let Some(get_func) = attr_class.get_attr("__get__") { + return call_descriptor(attr, get_func, self.into_object(), cls.clone(), vm); + } + } + } + + if let Some(obj_attr) = self.as_object().get_attr(&name.value) { + Ok(obj_attr) + } else if let Some(attr) = cls.get_attr(&name.value) { + let attr_class = attr.typ(); + if let Some(get_func) = attr_class.get_attr("__get__") { + call_descriptor(attr, get_func, self.into_object(), cls.clone(), vm) + } else { + Ok(attr) + } + } else if let Some(getter) = cls.get_attr("__getattr__") { + vm.invoke(getter, vec![self.into_object(), name.into_object()]) + } else { + Err(vm.new_attribute_error(format!("{} has no attribute '{}'", self.as_object(), name))) + } + } } fn none_new(vm: &mut VirtualMachine, args: PyFuncArgs) -> PyResult { @@ -55,5 +109,6 @@ pub fn init(context: &PyContext) { "__new__" => context.new_rustfunc(none_new), "__repr__" => context.new_rustfunc(PyNoneRef::repr), "__bool__" => context.new_rustfunc(PyNoneRef::bool), + "__getattribute__" => context.new_rustfunc(PyNoneRef::get_attribute) }); } diff --git a/vm/src/obj/objproperty.rs b/vm/src/obj/objproperty.rs index 060bb07fa..5369f274d 100644 --- a/vm/src/obj/objproperty.rs +++ b/vm/src/obj/objproperty.rs @@ -68,6 +68,19 @@ impl PyPropertyRef { // Descriptor methods + // specialised version that doesn't check for None + pub(crate) fn instance_binding_get( + self, + obj: PyObjectRef, + vm: &mut VirtualMachine, + ) -> PyResult { + if let Some(getter) = self.getter.as_ref() { + vm.invoke(getter.clone(), obj) + } else { + Err(vm.new_attribute_error("unreadable attribute".to_string())) + } + } + fn get(self, obj: PyObjectRef, _owner: PyClassRef, vm: &mut VirtualMachine) -> PyResult { if let Some(getter) = self.getter.as_ref() { if obj.is(&vm.ctx.none) {