mirror of https://github.com/astral-sh/ruff
[red-knot] Support custom `__getattr__` methods (#16668)
## Summary
Add support for calling custom `__getattr__` methods in case an
attribute is not otherwise found. This allows us to get rid of many
ecosystem false positives where we previously emitted errors when
accessing attributes on `argparse.Namespace`.
closes #16614
## Test Plan
* New Markdown tests
* Observed expected ecosystem changes (the changes for `arrow` also look
fine, since the `Arrow` class has a custom [`__getattr__`
here](1d70d00919/arrow/arrow.py (L802-L815)))
This commit is contained in:
parent
a176c1ac80
commit
083df0cf84
|
|
@ -1199,6 +1199,98 @@ reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A
|
||||||
reveal_type(C.x) # revealed: Literal[1] & Any
|
reveal_type(C.x) # revealed: Literal[1] & Any
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Classes with custom `__getattr__` methods
|
||||||
|
|
||||||
|
### Basic
|
||||||
|
|
||||||
|
If a type provides a custom `__getattr__` method, we use the return type of that method as the type
|
||||||
|
for unknown attributes. Consider the following `CustomGetAttr` class:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
def flag() -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
class GetAttrReturnType: ...
|
||||||
|
|
||||||
|
class CustomGetAttr:
|
||||||
|
class_attr: int = 1
|
||||||
|
|
||||||
|
if flag():
|
||||||
|
possibly_unbound: bytes = b"a"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.instance_attr: str = "a"
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> GetAttrReturnType:
|
||||||
|
return GetAttrReturnType()
|
||||||
|
```
|
||||||
|
|
||||||
|
We can access arbitrary attributes on instances of this class, and the type of the attribute will be
|
||||||
|
`GetAttrReturnType`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
c = CustomGetAttr()
|
||||||
|
|
||||||
|
reveal_type(c.whatever) # revealed: GetAttrReturnType
|
||||||
|
```
|
||||||
|
|
||||||
|
If an attribute is defined on the class, it takes precedence over the `__getattr__` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(c.class_attr) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
If the class attribute is possibly unbound, we union the type of the attribute with the fallback
|
||||||
|
type of the `__getattr__` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(c.possibly_unbound) # revealed: bytes | GetAttrReturnType
|
||||||
|
```
|
||||||
|
|
||||||
|
Instance attributes also take precedence over the `__getattr__` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# Note: we could attempt to union with the fallback type of `__getattr__` here, as we currently do not
|
||||||
|
# attempt to determine if instance attributes are always bound or not. Neither mypy nor pyright do this,
|
||||||
|
# so it's not a priority.
|
||||||
|
reveal_type(c.instance_attr) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type of the `name` parameter
|
||||||
|
|
||||||
|
If the `name` parameter of the `__getattr__` method is annotated with a (union of) literal type(s),
|
||||||
|
we only consider the attribute access to be valid if the accessed attribute is one of them:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
class Date:
|
||||||
|
def __getattr__(self, name: Literal["day", "month", "year"]) -> int:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
date = Date()
|
||||||
|
|
||||||
|
reveal_type(date.day) # revealed: int
|
||||||
|
reveal_type(date.month) # revealed: int
|
||||||
|
reveal_type(date.year) # revealed: int
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `Date` has no attribute `century`"
|
||||||
|
reveal_type(date.century) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
### `argparse.Namespace`
|
||||||
|
|
||||||
|
A standard library example of a class with a custom `__getattr__` method is `argparse.Namespace`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
def _(ns: argparse.Namespace):
|
||||||
|
reveal_type(ns.whatever) # revealed: Any
|
||||||
|
```
|
||||||
|
|
||||||
## Objects of all types have a `__class__` method
|
## Objects of all types have a `__class__` method
|
||||||
|
|
||||||
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
The type of `x.__class__` is the same as `x`'s meta-type. `x.__class__` is always the same value as
|
||||||
|
|
|
||||||
|
|
@ -2008,12 +2008,50 @@ impl<'db> Type<'db> {
|
||||||
| Type::FunctionLiteral(..) => {
|
| Type::FunctionLiteral(..) => {
|
||||||
let fallback = self.instance_member(db, name_str);
|
let fallback = self.instance_member(db, name_str);
|
||||||
|
|
||||||
self.invoke_descriptor_protocol(
|
let result = self.invoke_descriptor_protocol(
|
||||||
db,
|
db,
|
||||||
name_str,
|
name_str,
|
||||||
fallback,
|
fallback,
|
||||||
InstanceFallbackShadowsNonDataDescriptor::No,
|
InstanceFallbackShadowsNonDataDescriptor::No,
|
||||||
)
|
);
|
||||||
|
|
||||||
|
let custom_getattr_result =
|
||||||
|
|| {
|
||||||
|
// Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with dynamic imports.
|
||||||
|
// We explicitly hide it here to prevent arbitrary attributes from being available on modules.
|
||||||
|
if self.into_instance().is_some_and(|instance| {
|
||||||
|
instance.class.is_known(db, KnownClass::ModuleType)
|
||||||
|
}) {
|
||||||
|
return Symbol::Unbound.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.try_call_dunder(
|
||||||
|
db,
|
||||||
|
"__getattr__",
|
||||||
|
&CallArguments::positional([Type::StringLiteral(
|
||||||
|
StringLiteralType::new(db, Box::from(name.as_str())),
|
||||||
|
)]),
|
||||||
|
)
|
||||||
|
.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),
|
||||||
|
qualifiers: _,
|
||||||
|
} => member,
|
||||||
|
member @ SymbolAndQualifiers {
|
||||||
|
symbol: Symbol::Type(_, Boundness::PossiblyUnbound),
|
||||||
|
qualifiers: _,
|
||||||
|
} => member.or_fall_back_to(db, custom_getattr_result),
|
||||||
|
SymbolAndQualifiers {
|
||||||
|
symbol: Symbol::Unbound,
|
||||||
|
qualifiers: _,
|
||||||
|
} => custom_getattr_result(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue