mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:25:17 +00:00
[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:
parent
4ef2c223c9
commit
f885cb8a2f
2 changed files with 86 additions and 2 deletions
|
@ -1527,6 +1527,65 @@ def _(ns: argparse.Namespace):
|
||||||
reveal_type(ns.whatever) # revealed: Any
|
reveal_type(ns.whatever) # revealed: Any
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Classes with custom `__getattribute__` methods
|
||||||
|
|
||||||
|
If a type provides a custom `__getattribute__`, we use its return type as the type for unknown
|
||||||
|
attributes. Note that this behavior differs from runtime, where `__getattribute__` is called
|
||||||
|
unconditionally, even for known attributes. The rationale for doing this is that it allows users to
|
||||||
|
specify more precise types for specific attributes, such as `x: str` in the example below. This
|
||||||
|
behavior matches other type checkers such as mypy and pyright.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
x: str
|
||||||
|
def __getattribute__(self, attr: str) -> Any:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
reveal_type(Foo().x) # revealed: str
|
||||||
|
reveal_type(Foo().y) # revealed: Any
|
||||||
|
```
|
||||||
|
|
||||||
|
A standard library example for a class with a custom `__getattribute__` method is `SimpleNamespace`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
sn = SimpleNamespace(a="a")
|
||||||
|
|
||||||
|
reveal_type(sn.a) # revealed: Any
|
||||||
|
```
|
||||||
|
|
||||||
|
`__getattribute__` takes precedence over `__getattr__`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C:
|
||||||
|
def __getattribute__(self, name: str) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
c = C()
|
||||||
|
|
||||||
|
reveal_type(c.x) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
Like all dunder methods, `__getattribute__` is not looked up on instances:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def external_getattribute(name) -> int:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
class ThisFails:
|
||||||
|
def __init__(self):
|
||||||
|
self.__getattribute__ = external_getattribute
|
||||||
|
|
||||||
|
# error: [unresolved-attribute]
|
||||||
|
ThisFails().x
|
||||||
|
```
|
||||||
|
|
||||||
## Classes with custom `__setattr__` methods
|
## Classes with custom `__setattr__` methods
|
||||||
|
|
||||||
### Basic
|
### Basic
|
||||||
|
|
|
@ -3200,6 +3200,29 @@ impl<'db> Type<'db> {
|
||||||
.into()
|
.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 {
|
match result {
|
||||||
member @ SymbolAndQualifiers {
|
member @ SymbolAndQualifiers {
|
||||||
symbol: Symbol::Type(_, Boundness::Bound),
|
symbol: Symbol::Type(_, Boundness::Bound),
|
||||||
|
@ -3208,11 +3231,13 @@ impl<'db> Type<'db> {
|
||||||
member @ SymbolAndQualifiers {
|
member @ SymbolAndQualifiers {
|
||||||
symbol: Symbol::Type(_, Boundness::PossiblyUnbound),
|
symbol: Symbol::Type(_, Boundness::PossiblyUnbound),
|
||||||
qualifiers: _,
|
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 {
|
SymbolAndQualifiers {
|
||||||
symbol: Symbol::Unbound,
|
symbol: Symbol::Unbound,
|
||||||
qualifiers: _,
|
qualifiers: _,
|
||||||
} => custom_getattr_result(),
|
} => custom_getattribute_result().or_fall_back_to(db, custom_getattr_result),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue