Skip to content

Commit 9bacd19

Browse files
authored
[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!
1 parent f0fe6d6 commit 9bacd19

File tree

5 files changed

+44
-25
lines changed

5 files changed

+44
-25
lines changed

crates/ty_ide/src/completion.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1791,7 +1791,7 @@ quux.<CURSOR>
17911791
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
17921792
__module__ :: str
17931793
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
1794-
__new__ :: bound method Quux.__new__() -> Quux
1794+
__new__ :: def __new__(cls) -> Self@__new__
17951795
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
17961796
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
17971797
__repr__ :: bound method Quux.__repr__() -> str

crates/ty_python_semantic/resources/mdtest/call/methods.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,28 @@ reveal_type(C.f2(1)) # revealed: str
588588
reveal_type(C().f2(1)) # revealed: str
589589
```
590590

591+
### `__new__`
592+
593+
`__new__` is an implicit `@staticmethod`; accessing it on an instance does not bind the `cls`
594+
argument:
595+
596+
```py
597+
from typing_extensions import Self
598+
599+
reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__
600+
reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__
601+
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
602+
reveal_type(int.__new__)
603+
# revealed: Overload[(cls, x: @Todo(Support for `typing.TypeAlias`) = Literal[0], /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__]
604+
reveal_type((42).__new__)
605+
606+
class X:
607+
def __init__(self, val: int): ...
608+
def make_another(self) -> Self:
609+
reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__
610+
return self.__new__(X)
611+
```
612+
591613
## Builtin functions and methods
592614

593615
Some builtin functions and methods are heavily special-cased by ty. This mdtest checks that various

crates/ty_python_semantic/src/types.rs

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,16 +3584,21 @@ impl<'db> Type<'db> {
35843584
}
35853585
}
35863586

3587-
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
3588-
#[allow(unused_variables)]
3589-
// If we choose name `_unit`, the macro will generate code that uses `_unit`, causing clippy to fail.
3590-
fn lookup_dunder_new(self, db: &'db dyn Db, unit: ()) -> Option<PlaceAndQualifiers<'db>> {
3591-
self.find_name_in_mro_with_policy(
3592-
db,
3593-
"__new__",
3594-
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
3595-
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
3596-
)
3587+
fn lookup_dunder_new(self, db: &'db dyn Db) -> Option<PlaceAndQualifiers<'db>> {
3588+
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
3589+
fn lookup_dunder_new_inner<'db>(
3590+
db: &'db dyn Db,
3591+
ty: Type<'db>,
3592+
_: (),
3593+
) -> Option<PlaceAndQualifiers<'db>> {
3594+
let mut flags = MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK;
3595+
if !ty.is_subtype_of(db, KnownClass::Type.to_instance(db)) {
3596+
flags |= MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK;
3597+
}
3598+
ty.find_name_in_mro_with_policy(db, "__new__", flags)
3599+
}
3600+
3601+
lookup_dunder_new_inner(db, self, ())
35973602
}
35983603

35993604
/// 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> {
60896094
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
60906095
// easy to check if that's the one we found?
60916096
// Note that `__new__` is a static method, so we must inject the `cls` argument.
6092-
let new_method = self_type.lookup_dunder_new(db, ());
6097+
let new_method = self_type.lookup_dunder_new(db);
60936098

60946099
// Construct an instance type that we can use to look up the `__init__` instance method.
60956100
// This performs the same logic as `Type::to_instance`, except for generic class literals.

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ use crate::types::diagnostic::{
2525
};
2626
use crate::types::enums::is_enum_class;
2727
use crate::types::function::{
28-
DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionType,
29-
KnownFunction, OverloadLiteral,
28+
DataclassTransformerFlags, DataclassTransformerParams, FunctionType, KnownFunction,
29+
OverloadLiteral,
3030
};
3131
use crate::types::generics::{
3232
InferableTypeVars, Specialization, SpecializationBuilder, SpecializationError,
@@ -357,9 +357,7 @@ impl<'db> Bindings<'db> {
357357

358358
_ => {}
359359
}
360-
} else if function
361-
.has_known_decorator(db, FunctionDecorators::STATICMETHOD)
362-
{
360+
} else if function.is_staticmethod(db) {
363361
overload.set_return_type(*function_ty);
364362
} else {
365363
match overload.parameter_types() {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,16 +1051,10 @@ impl<'db> ClassType<'db> {
10511051
return Type::Callable(metaclass_dunder_call_function.into_callable_type(db));
10521052
}
10531053

1054-
let dunder_new_function_symbol = self_ty
1055-
.member_lookup_with_policy(
1056-
db,
1057-
"__new__".into(),
1058-
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
1059-
)
1060-
.place;
1054+
let dunder_new_function_symbol = self_ty.lookup_dunder_new(db);
10611055

10621056
let dunder_new_signature = dunder_new_function_symbol
1063-
.ignore_possibly_undefined()
1057+
.and_then(|place_and_quals| place_and_quals.ignore_possibly_undefined())
10641058
.and_then(|ty| match ty {
10651059
Type::FunctionLiteral(function) => Some(function.signature(db)),
10661060
Type::Callable(callable) => Some(callable.signatures(db)),

0 commit comments

Comments
 (0)