From 53ffe7143f4f09a09385f81456376829398d4bb8 Mon Sep 17 00:00:00 2001 From: Matthew Mckee Date: Mon, 21 Apr 2025 23:29:36 +0100 Subject: [PATCH] [red-knot] Add basic subtyping between class literal and callable (#17469) ## Summary This covers step 1 from https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable Part of #17343 ## Test Plan Update is_subtype_of.md and is_assignable_to.md --------- Co-authored-by: Carl Meyer --- .../mdtest/type_properties/is_subtype_of.md | 41 +++++++++++++++++++ crates/red_knot_python_semantic/src/types.rs | 21 ++++++++++ 2 files changed, 62 insertions(+) diff --git a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md index c61f748598..6eb506e602 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md +++ b/crates/red_knot_python_semantic/resources/mdtest/type_properties/is_subtype_of.md @@ -1125,6 +1125,47 @@ def f(fn: Callable[[int], int]) -> None: ... f(a) ``` +### Class literals + +#### Classes with metaclasses + +```py +from typing import Callable, overload +from typing_extensions import Self +from knot_extensions import TypeOf, static_assert, is_subtype_of + +class MetaWithReturn(type): + def __call__(cls) -> "A": + return super().__call__() + +class A(metaclass=MetaWithReturn): ... + +static_assert(is_subtype_of(TypeOf[A], Callable[[], A])) +static_assert(not is_subtype_of(TypeOf[A], Callable[[object], A])) + +class MetaWithDifferentReturn(type): + def __call__(cls) -> int: + return super().__call__() + +class B(metaclass=MetaWithDifferentReturn): ... + +static_assert(is_subtype_of(TypeOf[B], Callable[[], int])) +static_assert(not is_subtype_of(TypeOf[B], Callable[[], B])) + +class MetaWithOverloadReturn(type): + @overload + def __call__(cls, x: int) -> int: ... + @overload + def __call__(cls) -> str: ... + def __call__(cls, x: int | None = None) -> str | int: + return super().__call__() + +class C(metaclass=MetaWithOverloadReturn): ... + +static_assert(is_subtype_of(TypeOf[C], Callable[[int], int])) +static_assert(is_subtype_of(TypeOf[C], Callable[[], str])) +``` + ### Bound methods ```py diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index b57443a5e4..c7464b8e9c 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1134,6 +1134,27 @@ impl<'db> Type<'db> { self_subclass_ty.is_subtype_of(db, target_subclass_ty) } + (Type::ClassLiteral(_), Type::Callable(_)) => { + let metaclass_call_symbol = self + .member_lookup_with_policy( + db, + "__call__".into(), + MemberLookupPolicy::NO_INSTANCE_FALLBACK + | MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK, + ) + .symbol; + + if let Symbol::Type(Type::BoundMethod(new_function), _) = metaclass_call_symbol { + // TODO: this intentionally diverges from step 1 in + // https://typing.python.org/en/latest/spec/constructors.html#converting-a-constructor-to-callable + // by always respecting the signature of the metaclass `__call__`, rather than + // using a heuristic which makes unwarranted assumptions to sometimes ignore it. + let new_function = new_function.into_callable_type(db); + return new_function.is_subtype_of(db, target); + } + false + } + // `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`.