[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 <carl@astral.sh>
This commit is contained in:
Matthew Mckee 2025-04-21 23:29:36 +01:00 committed by GitHub
parent 21561000b1
commit 53ffe7143f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 0 deletions

View file

@ -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

View file

@ -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`.