[ty] use __getattribute__ to lookup unknown members on a type (#18280)

## Summary

`Type::member_lookup_with_policy` now falls back to calling
`__getattribute__` when a member cannot be found as a second fallback
after `__getattr__`.


closes https://github.com/astral-sh/ty/issues/441

## Test Plan

Added markdown tests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Felix Scherz 2025-05-26 12:59:45 +02:00 committed by GitHub
parent 4ef2c223c9
commit f885cb8a2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 2 deletions

View file

@ -3200,6 +3200,29 @@ impl<'db> Type<'db> {
.into()
};
let custom_getattribute_result = || {
// Avoid cycles when looking up `__getattribute__`
if "__getattribute__" == name.as_str() {
return Symbol::Unbound.into();
}
// Typeshed has a `__getattribute__` method defined on `builtins.object` so we
// explicitly hide it here using `MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK`.
self.try_call_dunder_with_policy(
db,
"__getattribute__",
&mut CallArgumentTypes::positional([Type::StringLiteral(
StringLiteralType::new(db, Box::from(name.as_str())),
)]),
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
| MemberLookupPolicy::NO_INSTANCE_FALLBACK,
)
.map(|outcome| Symbol::bound(outcome.return_type(db)))
// TODO: Handle call errors here.
.unwrap_or(Symbol::Unbound)
.into()
};
match result {
member @ SymbolAndQualifiers {
symbol: Symbol::Type(_, Boundness::Bound),
@ -3208,11 +3231,13 @@ impl<'db> Type<'db> {
member @ SymbolAndQualifiers {
symbol: Symbol::Type(_, Boundness::PossiblyUnbound),
qualifiers: _,
} => member.or_fall_back_to(db, custom_getattr_result),
} => member
.or_fall_back_to(db, custom_getattribute_result)
.or_fall_back_to(db, custom_getattr_result),
SymbolAndQualifiers {
symbol: Symbol::Unbound,
qualifiers: _,
} => custom_getattr_result(),
} => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result),
}
}