From f32f7a3b48ff11351c709c60109fafd5ca18ec6c Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Wed, 9 Jul 2025 09:04:55 +0100 Subject: [PATCH] [ty] Fix `ClassLiteral.into_callable` for dataclasses (#19192) ## Summary Change `ClassLiteral.into_callable` to also look for `__init__` functions of type `Type::Callable` (such as synthesized `__init__` functions of dataclasses). Fixes https://github.com/astral-sh/ty/issues/760 ## Test Plan Add subtype test --------- Co-authored-by: David Peter --- .../mdtest/type_properties/is_subtype_of.md | 19 ++++++++ crates/ty_python_semantic/src/types/class.rs | 44 +++++++++++-------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index 51756bac64..5726ce83f7 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1774,6 +1774,25 @@ static_assert(is_subtype_of(type[B], Callable[[str], B])) static_assert(not is_subtype_of(type[B], Callable[[int], B])) ``` +### Dataclasses + +Dataclasses synthesize a `__init__` method. + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of +from dataclasses import dataclass + +@dataclass +class A: + x: "A" | None + +static_assert(is_subtype_of(type[A], Callable[[A], A])) +static_assert(is_subtype_of(type[A], Callable[[None], A])) +static_assert(is_subtype_of(type[A], Callable[[A | None], A])) +static_assert(not is_subtype_of(type[A], Callable[[int], A])) +``` + ### Bound methods ```py diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index fd64f5e204..23f91a01cb 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -661,27 +661,33 @@ impl<'db> ClassType<'db> { // same parameters as the `__init__` method after it is bound, and with the return type of // the concrete type of `Self`. let synthesized_dunder_init_callable = - if let Place::Type(Type::FunctionLiteral(dunder_init_function), _) = - dunder_init_function_symbol - { - let synthesized_signature = |signature: Signature<'db>| { - Signature::new(signature.parameters().clone(), Some(correct_return_type)) - .bind_self() + if let Place::Type(ty, _) = dunder_init_function_symbol { + let signature = match ty { + Type::FunctionLiteral(dunder_init_function) => { + Some(dunder_init_function.signature(db)) + } + Type::Callable(callable) => Some(callable.signatures(db)), + _ => None, }; - let synthesized_dunder_init_signature = CallableSignature::from_overloads( - dunder_init_function - .signature(db) - .overloads - .iter() - .cloned() - .map(synthesized_signature), - ); - Some(Type::Callable(CallableType::new( - db, - synthesized_dunder_init_signature, - true, - ))) + if let Some(signature) = signature { + let synthesized_signature = |signature: &Signature<'db>| { + Signature::new(signature.parameters().clone(), Some(correct_return_type)) + .bind_self() + }; + + let synthesized_dunder_init_signature = CallableSignature::from_overloads( + signature.overloads.iter().map(synthesized_signature), + ); + + Some(Type::Callable(CallableType::new( + db, + synthesized_dunder_init_signature, + true, + ))) + } else { + None + } } else { None };