From 352b896c897a0787a5302517c53c6c371aeac8df Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Thu, 3 Jul 2025 03:22:31 +0100 Subject: [PATCH] [ty] Add subtyping between SubclassOf and CallableType (#19026) ## Summary Part of https://github.com/astral-sh/ty/issues/129 There were previously some false positives here. ## Test Plan Updated `is_subtype_of.md` and `is_assignable_to.md` --- .../type_properties/is_assignable_to.md | 31 +++++++++++++++++++ .../mdtest/type_properties/is_subtype_of.md | 22 +++++++++++++ crates/ty_python_semantic/src/types.rs | 10 ++++++ 3 files changed, 63 insertions(+) diff --git a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md index ee5ce9c8da..ab730adb68 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md +++ b/crates/ty_python_semantic/resources/mdtest/type_properties/is_assignable_to.md @@ -1064,6 +1064,37 @@ static_assert(not is_assignable_to(A, Callable[[int], int])) reveal_type(A()(1)) # revealed: str ``` +### Subclass of + +#### Type of a class with constructor methods + +```py +from typing import Callable +from ty_extensions import static_assert, is_assignable_to + +class A: + def __init__(self, x: int) -> None: ... + +class B: + def __new__(cls, x: str) -> "B": + return super().__new__(cls) + +static_assert(is_assignable_to(type[A], Callable[[int], A])) +static_assert(not is_assignable_to(type[A], Callable[[str], A])) + +static_assert(is_assignable_to(type[B], Callable[[str], B])) +static_assert(not is_assignable_to(type[B], Callable[[int], B])) +``` + +#### Type with no generic parameters + +```py +from typing import Callable, Any +from ty_extensions import static_assert, is_assignable_to + +static_assert(is_assignable_to(type, Callable[..., Any])) +``` + ## Generics ### Assignability of generic types parameterized by gradual types 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 7476b57c0f..51756bac64 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 @@ -1752,6 +1752,28 @@ static_assert(not is_subtype_of(TypeOf[F], Callable[[], str])) static_assert(not is_subtype_of(TypeOf[F], Callable[[int], F])) ``` +### Subclass of + +#### Type of a class with constructor methods + +```py +from typing import Callable +from ty_extensions import TypeOf, static_assert, is_subtype_of + +class A: + def __init__(self, x: int) -> None: ... + +class B: + def __new__(cls, x: str) -> "B": + return super().__new__(cls) + +static_assert(is_subtype_of(type[A], Callable[[int], A])) +static_assert(not is_subtype_of(type[A], Callable[[str], A])) + +static_assert(is_subtype_of(type[B], Callable[[str], B])) +static_assert(not is_subtype_of(type[B], Callable[[int], B])) +``` + ### Bound methods ```py diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index f9835e7db8..42db465d39 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1559,6 +1559,16 @@ impl<'db> Type<'db> { .into_callable(db) .has_relation_to(db, target, relation), + // TODO: This is unsound so in future we can consider an opt-in option to disable it. + (Type::SubclassOf(subclass_of_ty), Type::Callable(_)) + if subclass_of_ty.subclass_of().into_class().is_some() => + { + let class = subclass_of_ty.subclass_of().into_class().unwrap(); + class + .into_callable(db) + .has_relation_to(db, target, relation) + } + // `Literal[str]` is a subtype of `type` because the `str` class object is an instance of its metaclass `type`. // `Literal[abc.ABC]` is a subtype of `abc.ABCMeta` because the `abc.ABC` class object // is an instance of its metaclass `abc.ABCMeta`.