mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Use non-raising lookup for default optional attribute access (#8576)
* vm: use non-raising lookup for default attribute access get_attribute_opt() backs hasattr() and getattr() with a default. It used get_attr_inner() for every object, then converted AttributeError from a missing attribute into None. The usual user-object miss therefore built and dropped an exception just to answer an optional lookup. Route objects using PyBaseObject::getattro through generic_getattr_opt(), which returns the missing case without constructing AttributeError. Objects with a custom getattro slot keep the raising path: their __getattribute__ and __getattr__ hooks can run Python code, so their exceptions must preserve the existing behavior. AttributeError from either route, including a descriptor, is still converted to None; non-AttributeError exceptions propagate. The new snippet covers ordinary hits and misses, custom __getattribute__ / __getattr__, descriptor exceptions, and dynamic getattro-slot updates. Refs youknowone#48 Assisted-by: Codex:GPT-5.6-terra * vm: use non-raising lookup for optional attributes
This commit is contained in:
1 parent
50b60dc85c
commit
dd32207408
2 files changed
+137
-4
No files matched your search
+11
-4
@@ -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<Option<PyObjectRef>> {
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
Reference in new issue
Block a user