Files
RustPython/extra_tests/snippets/builtin_optional_attr.py
이광효 dd32207408 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
2026-08-22 20:55:24 +09:00

127 lines
3.5 KiB
Python

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"]