diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index a26ab3761..28e59a2f4 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -23,8 +23,8 @@ mod vm_ops; use crate::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, builtins::{ - self, PyBaseExceptionRef, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr, PyStrInterned, - PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned, PyWeak, + self, PyBaseExceptionRef, PyBaseObject, PyDict, PyDictRef, PyInt, PyList, PyModule, PyStr, + PyStrInterned, PyStrRef, PyTypeRef, PyUtf8Str, PyUtf8StrInterned, PyWeak, code::PyCode, dict::{PyDictItems, PyDictKeys, PyDictValues}, pystr::AsPyStr, @@ -42,6 +42,7 @@ use crate::{ scope::Scope, signal::{self, SignalHandlers}, stdlib, + types::{GetattroFunc, fn_addr}, warn::WarningsState, }; use alloc::{borrow::Cow, collections::BTreeMap}; @@ -2854,8 +2855,14 @@ impl VirtualMachine { attr_name: impl AsPyStr<'a>, ) -> PyResult> { let attr_name = attr_name.as_pystr(&self.ctx); - match obj.get_attr_inner(attr_name, self) { - Ok(attr) => Ok(Some(attr)), + let getattro = obj.class().slots.getattro.load().unwrap(); + let result = if fn_addr(getattro) == fn_addr(PyBaseObject::getattro as GetattroFunc) { + obj.generic_getattr_opt(attr_name, None, self) + } else { + obj.get_attr_inner(attr_name, self).map(Some) + }; + match result { + Ok(attr) => Ok(attr), Err(e) if e.fast_isinstance(self.ctx.exceptions.attribute_error) => Ok(None), Err(e) => Err(e), } diff --git a/extra_tests/snippets/builtin_optional_attr.py b/extra_tests/snippets/builtin_optional_attr.py new file mode 100644 index 000000000..3e501b3cd --- /dev/null +++ b/extra_tests/snippets/builtin_optional_attr.py @@ -0,0 +1,126 @@ +def assert_raises(exc_type, func, *args): + try: + func(*args) + except exc_type: + return + except BaseException as exc: + raise AssertionError( + f"expected {exc_type.__name__}, got {type(exc).__name__}" + ) from exc + raise AssertionError(f"expected {exc_type.__name__}") + + +class Plain: + class_attr = "class" + + +plain = Plain() +plain.instance_attr = "instance" +sentinel = object() + +assert hasattr(plain, "class_attr") +assert hasattr(plain, "instance_attr") +assert not hasattr(plain, "missing") +assert getattr(plain, "class_attr", sentinel) == "class" +assert getattr(plain, "instance_attr", sentinel) == "instance" +assert getattr(plain, "missing", sentinel) is sentinel +assert_raises(AttributeError, getattr, plain, "missing") +assert_raises(TypeError, hasattr, plain, 1) +assert_raises(TypeError, getattr, plain, 1, sentinel) + + +getattribute_calls = [] + + +class CustomGetattribute: + def __getattribute__(self, name): + getattribute_calls.append(name) + if name == "missing": + raise AttributeError("custom miss") + if name == "bad": + raise ValueError("custom error") + return f"value:{name}" + + +custom_getattribute = CustomGetattribute() +assert not hasattr(custom_getattribute, "missing") +assert getattr(custom_getattribute, "missing", sentinel) is sentinel +assert getattribute_calls == ["missing", "missing"] +assert getattr(custom_getattribute, "present", sentinel) == "value:present" +assert_raises(ValueError, hasattr, custom_getattribute, "bad") + + +getattr_calls = [] + + +class CustomGetattr: + def __getattr__(self, name): + getattr_calls.append(name) + if name == "present": + return "fallback value" + if name == "bad": + raise SystemExit("custom error") + raise AttributeError(name) + + +custom_getattr = CustomGetattr() +assert getattr(custom_getattr, "present", sentinel) == "fallback value" +assert not hasattr(custom_getattr, "missing") +assert getattr_calls == ["present", "missing"] +assert_raises(SystemExit, hasattr, custom_getattr, "bad") + + +class AttributeErrorSubclass(AttributeError): + pass + + +class AttributeErrorDescriptor: + def __get__(self, obj, owner): + raise AttributeError("descriptor miss") + + +class AttributeErrorSubclassDescriptor: + def __get__(self, obj, owner): + raise AttributeErrorSubclass("descriptor miss") + + +class ValueErrorDescriptor: + def __get__(self, obj, owner): + raise ValueError("descriptor error") + + +class DescriptorContainer: + attribute_error = AttributeErrorDescriptor() + attribute_error_subclass = AttributeErrorSubclassDescriptor() + value_error = ValueErrorDescriptor() + + +descriptor_container = DescriptorContainer() +assert not hasattr(descriptor_container, "attribute_error") +assert getattr(descriptor_container, "attribute_error", sentinel) is sentinel +assert not hasattr(descriptor_container, "attribute_error_subclass") +assert getattr(descriptor_container, "attribute_error_subclass", sentinel) is sentinel +assert_raises(ValueError, hasattr, descriptor_container, "value_error") + + +class Dynamic: + pass + + +dynamic = Dynamic() +assert not hasattr(dynamic, "missing") +dynamic_calls = [] + + +def dynamic_getattribute(self, name): + dynamic_calls.append(name) + if name == "missing": + raise AttributeError(name) + return object.__getattribute__(self, name) + + +Dynamic.__getattribute__ = dynamic_getattribute +assert not hasattr(dynamic, "missing") +del Dynamic.__getattribute__ +assert not hasattr(dynamic, "missing") +assert dynamic_calls == ["missing"]