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 {