mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-22 13:23:18 +00:00
[ty] Fix lookup of __new__ on instances (#21147)
## Summary We weren't correctly modeling it as a `staticmethod` in all cases, leading us to incorrectly infer that the `cls` argument would be bound if it was accessed on an instance (rather than the class object). ## Test Plan Added mdtests that fail on `main`. The primer output also looks good!
This commit is contained in:
parent
f0fe6d62fb
commit
9bacd19c5a
5 changed files with 44 additions and 25 deletions
|
|
@ -1791,7 +1791,7 @@ quux.<CURSOR>
|
||||||
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
||||||
__module__ :: str
|
__module__ :: str
|
||||||
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
||||||
__new__ :: bound method Quux.__new__() -> Quux
|
__new__ :: def __new__(cls) -> Self@__new__
|
||||||
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
||||||
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
||||||
__repr__ :: bound method Quux.__repr__() -> str
|
__repr__ :: bound method Quux.__repr__() -> str
|
||||||
|
|
|
||||||
|
|
@ -588,6 +588,28 @@ reveal_type(C.f2(1)) # revealed: str
|
||||||
reveal_type(C().f2(1)) # revealed: str
|
reveal_type(C().f2(1)) # revealed: str
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `__new__`
|
||||||
|
|
||||||
|
`__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls`
|
||||||
|
argument:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||||
|
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||||
|
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||||
|
reveal_type(int.__new__)
|
||||||
|
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
|
||||||
|
reveal_type((42).__new__)
|
||||||
|
|
||||||
|
class X:
|
||||||
|
def __init__(self, val: int): ...
|
||||||
|
def make_another(self) -> Self:
|
||||||
|
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
|
||||||
|
return self.__new__(X)
|
||||||
|
```
|
||||||
|
|
||||||
## Builtin functions and methods
|
## Builtin functions and methods
|
||||||
|
|
||||||
Some builtin functions and methods are heavily special-cased by ty. This mdtest checks that various
|
Some builtin functions and methods are heavily special-cased by ty. This mdtest checks that various
|
||||||
|
|
|
||||||
|
|
@ -3584,16 +3584,21 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
fn lookup_dunder_new(self, db: &'db dyn Db) -> Option<PlaceAndQualifiers<'db>> {
|
||||||
#[allow(unused_variables)]
|
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||||
// If we choose name `_unit`, the macro will generate code that uses `_unit`, causing clippy to fail.
|
fn lookup_dunder_new_inner<'db>(
|
||||||
fn lookup_dunder_new(self, db: &'db dyn Db, unit: ()) -> Option<PlaceAndQualifiers<'db>> {
|
db: &'db dyn Db,
|
||||||
self.find_name_in_mro_with_policy(
|
ty: Type<'db>,
|
||||||
db,
|
_: (),
|
||||||
"__new__",
|
) -> Option<PlaceAndQualifiers<'db>> {
|
||||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
|
let mut flags = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK;
|
||||||
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
|
if !ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) {
|
||||||
)
|
flags |= MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK;
|
||||||
|
}
|
||||||
|
ty.find_name_in_mro_with_policy(db, "__new__", flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup_dunder_new_inner(db, self, ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes
|
/// Look up an attribute in the MRO of the meta-type of `self`. This returns class-level attributes
|
||||||
|
|
@ -6089,7 +6094,7 @@ impl<'db> Type<'db> {
|
||||||
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
|
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
|
||||||
// easy to check if that's the one we found?
|
// easy to check if that's the one we found?
|
||||||
// Note that `__new__` is a static method, so we must inject the `cls` argument.
|
// Note that `__new__` is a static method, so we must inject the `cls` argument.
|
||||||
let new_method = self_type.lookup_dunder_new(db, ());
|
let new_method = self_type.lookup_dunder_new(db);
|
||||||
|
|
||||||
// Construct an instance type that we can use to look up the `__init__` instance method.
|
// Construct an instance type that we can use to look up the `__init__` instance method.
|
||||||
// This performs the same logic as `Type::to_instance`, except for generic class literals.
|
// This performs the same logic as `Type::to_instance`, except for generic class literals.
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ use crate::types::diagnostic::{
|
||||||
};
|
};
|
||||||
use crate::types::enums::is_enum_class;
|
use crate::types::enums::is_enum_class;
|
||||||
use crate::types::function::{
|
use crate::types::function::{
|
||||||
DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionType,
|
DataclassTransformerFlags, DataclassTransformerParams, FunctionType, KnownFunction,
|
||||||
KnownFunction, OverloadLiteral,
|
OverloadLiteral,
|
||||||
};
|
};
|
||||||
use crate::types::generics::{
|
use crate::types::generics::{
|
||||||
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
|
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
|
||||||
|
|
@ -357,9 +357,7 @@ impl<'db> Bindings<'db> {
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if function
|
} else if function.is_staticmethod(db) {
|
||||||
.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
|
|
||||||
{
|
|
||||||
overload.set_return_type(*function_ty);
|
overload.set_return_type(*function_ty);
|
||||||
} else {
|
} else {
|
||||||
match overload.parameter_types() {
|
match overload.parameter_types() {
|
||||||
|
|
|
||||||
|
|
@ -1051,16 +1051,10 @@ impl<'db> ClassType<'db> {
|
||||||
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
|
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
|
||||||
}
|
}
|
||||||
|
|
||||||
let dunder_new_function_symbol = self_ty
|
let dunder_new_function_symbol = self_ty.lookup_dunder_new(db);
|
||||||
.member_lookup_with_policy(
|
|
||||||
db,
|
|
||||||
"__new__".into(),
|
|
||||||
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
|
||||||
)
|
|
||||||
.place;
|
|
||||||
|
|
||||||
let dunder_new_signature = dunder_new_function_symbol
|
let dunder_new_signature = dunder_new_function_symbol
|
||||||
.ignore_possibly_undefined()
|
.and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined())
|
||||||
.and_then(|ty| match ty {
|
.and_then(|ty| match ty {
|
||||||
Type::FunctionLiteral(function) => Some(function.signature(db)),
|
Type::FunctionLiteral(function) => Some(function.signature(db)),
|
||||||
Type::Callable(callable) => Some(callable.signatures(db)),
|
Type::Callable(callable) => Some(callable.signatures(db)),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue