Skip to content

Commit 0577d51

Browse files
committed
fix latent bug regarding synthesized __new__ methods for class literals
1 parent 7a24e6d commit 0577d51

File tree

5 files changed

+53
-38
lines changed

5 files changed

+53
-38
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,7 @@ c: Callable[[Any], str] = A().g
912912

913913
```py
914914
from typing import Any, Callable
915+
from ty_extensions import static_assert, is_assignable_to
915916

916917
c: Callable[[object], type] = type
917918
c: Callable[[str], Any] = str
@@ -932,6 +933,15 @@ class C:
932933
def __init__(self, x: int) -> None: ...
933934

934935
c: Callable[[int], C] = C
936+
937+
def f(a: Callable[..., Any], b: Callable[[Any], Any]): ...
938+
939+
f(tuple, tuple)
940+
941+
def g(a: Callable[[Any, Any], Any]): ...
942+
943+
# error: [invalid-argument-type] "Argument to function `g` is incorrect: Expected `(Any, Any, /) -> Any`, found `<class 'tuple'>`"
944+
g(tuple)
935945
```
936946

937947
### Generic class literal types

crates/ty_python_semantic/src/types.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4585,7 +4585,7 @@ impl<'db> Type<'db> {
45854585
}
45864586

45874587
if let Type::GenericAlias(alias) = self {
4588-
if alias.origin(db).is_known(db, KnownClass::Tuple) {
4588+
if alias.origin(db).is_tuple(db) {
45894589
return Ok(todo_type!("*tuple[] annotations"));
45904590
}
45914591
}

crates/ty_python_semantic/src/types/class.rs

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -586,10 +586,8 @@ impl<'db> ClassType<'db> {
586586
Place::bound(synthesized_dunder_method).into()
587587
};
588588

589-
let is_tuple = || class_literal.is_known(db, KnownClass::Tuple);
590-
591589
match name {
592-
"__len__" if is_tuple() => {
590+
"__len__" if class_literal.is_tuple(db) => {
593591
let return_type = specialization
594592
.and_then(|spec| spec.tuple(db).len().into_fixed_length())
595593
.and_then(|len| i64::try_from(len).ok())
@@ -599,7 +597,7 @@ impl<'db> ClassType<'db> {
599597
synthesize_simple_tuple_method(return_type)
600598
}
601599

602-
"__bool__" if is_tuple() => {
600+
"__bool__" if class_literal.is_tuple(db) => {
603601
let return_type = specialization
604602
.map(|spec| spec.tuple(db).truthiness().into_type(db))
605603
.unwrap_or_else(|| KnownClass::Bool.to_instance(db));
@@ -614,7 +612,7 @@ impl<'db> ClassType<'db> {
614612
// @overload
615613
// def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ...
616614
// ```
617-
"__new__" if is_tuple() => {
615+
"__new__" if class_literal.is_tuple(db) => {
618616
let mut iterable_parameter =
619617
Parameter::positional_only(Some(Name::new_static("iterable")));
620618

@@ -727,38 +725,41 @@ impl<'db> ClassType<'db> {
727725
)
728726
.place;
729727

730-
let dunder_new_function =
731-
if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) =
732-
dunder_new_function_symbol
733-
{
734-
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
735-
// then we should ignore the `__init__` and just return the `__new__` method.
736-
let returns_non_subclass =
737-
dunder_new_function
738-
.signature(db)
739-
.overloads
740-
.iter()
741-
.any(|signature| {
742-
signature.return_ty.is_some_and(|return_ty| {
743-
!return_ty.is_assignable_to(
744-
db,
745-
self_ty
746-
.to_instance(db)
747-
.expect("ClassType should be instantiable"),
748-
)
749-
})
750-
});
728+
let dunder_new_signature = dunder_new_function_symbol
729+
.ignore_possibly_unbound()
730+
.and_then(|ty| match ty {
731+
Type::FunctionLiteral(function) => Some(function.signature(db)),
732+
Type::Callable(callable) => Some(callable.signatures(db)),
733+
_ => None,
734+
});
751735

752-
let dunder_new_bound_method =
753-
dunder_new_function.into_bound_method_type(db, self_ty);
736+
let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature {
737+
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
738+
// then we should ignore the `__init__` and just return the `__new__` method.
739+
let returns_non_subclass = dunder_new_signature.overloads.iter().any(|signature| {
740+
signature.return_ty.is_some_and(|return_ty| {
741+
!return_ty.is_assignable_to(
742+
db,
743+
self_ty
744+
.to_instance(db)
745+
.expect("ClassType should be instantiable"),
746+
)
747+
})
748+
});
754749

755-
if returns_non_subclass {
756-
return dunder_new_bound_method;
757-
}
758-
Some(dunder_new_bound_method)
759-
} else {
760-
None
761-
};
750+
let dunder_new_bound_method = Type::Callable(CallableType::new(
751+
db,
752+
dunder_new_signature.bind_self(),
753+
true,
754+
));
755+
756+
if returns_non_subclass {
757+
return dunder_new_bound_method;
758+
}
759+
Some(dunder_new_bound_method)
760+
} else {
761+
None
762+
};
762763

763764
let dunder_init_function_symbol = self_ty
764765
.member_lookup_with_policy(
@@ -932,6 +933,10 @@ impl<'db> ClassLiteral<'db> {
932933
self.known(db) == Some(known_class)
933934
}
934935

936+
pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool {
937+
self.is_known(db, KnownClass::Tuple)
938+
}
939+
935940
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
936941
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
937942
// the knowledge that this class is not generic.

crates/ty_python_semantic/src/types/display.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@ impl TupleSpecialization {
511511
}
512512

513513
fn from_class(db: &dyn Db, class: ClassLiteral) -> Self {
514-
if class.is_known(db, KnownClass::Tuple) {
514+
if class.is_tuple(db) {
515515
Self::Yes
516516
} else {
517517
Self::No

crates/ty_python_semantic/src/types/infer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7995,7 +7995,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
79957995
// updating all of the subscript logic below to use custom callables for all of the _other_
79967996
// special cases, too.
79977997
if let Type::ClassLiteral(class) = value_ty {
7998-
if class.is_known(self.db(), KnownClass::Tuple) {
7998+
if class.is_tuple(self.db()) {
79997999
return self
80008000
.infer_tuple_type_expression(slice)
80018001
.to_meta_type(self.db());

0 commit comments

Comments
 (0)