[ty] Add subtyping between Callable types and class literals with __init__ (#17638)

## Summary

Allow classes with `__init__` to be subtypes of `Callable`

Fixes https://github.com/astral-sh/ty/issues/358

## Test Plan

Update is_subtype_of.md

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Matthew Mckee 2025-05-28 21:43:07 +01:00 committed by GitHub
parent 16621fa19d
commit c60b4d7f30
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 329 additions and 46 deletions

View file

@ -608,11 +608,49 @@ c: Callable[[Any], str] = A().g
```py
from typing import Any, Callable
c: Callable[[object], type] = type
c: Callable[[str], Any] = str
c: Callable[[str], Any] = int
# error: [invalid-assignment]
c: Callable[[str], Any] = object
class A:
def __init__(self, x: int) -> None: ...
a: Callable[[int], A] = A
class C:
def __new__(cls, *args, **kwargs) -> "C":
return super().__new__(cls)
def __init__(self, x: int) -> None: ...
c: Callable[[int], C] = C
```
### Generic class literal types
```toml
[environment]
python-version = "3.12"
```
```py
from typing import Callable
class B[T]:
def __init__(self, x: T) -> None: ...
b: Callable[[int], B[int]] = B[int]
class C[T]:
def __new__(cls, *args, **kwargs) -> "C[T]":
return super().__new__(cls)
def __init__(self, x: T) -> None: ...
c: Callable[[int], C[int]] = C[int]
```
### Overloads

View file

@ -1219,7 +1219,7 @@ static_assert(is_subtype_of(TypeOf[C], Callable[[], str]))
#### Classes with `__new__`
```py
from typing import Callable
from typing import Callable, overload
from ty_extensions import TypeOf, static_assert, is_subtype_of
class A:
@ -1244,6 +1244,20 @@ static_assert(is_subtype_of(TypeOf[E], Callable[[], C]))
static_assert(is_subtype_of(TypeOf[E], Callable[[], B]))
static_assert(not is_subtype_of(TypeOf[D], Callable[[], C]))
static_assert(is_subtype_of(TypeOf[D], Callable[[], B]))
class F:
@overload
def __new__(cls) -> int: ...
@overload
def __new__(cls, x: int) -> "F": ...
def __new__(cls, x: int | None = None) -> "int | F":
return 1 if x is None else object.__new__(cls)
def __init__(self, y: str) -> None: ...
static_assert(is_subtype_of(TypeOf[F], Callable[[int], F]))
static_assert(is_subtype_of(TypeOf[F], Callable[[], int]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[str], F]))
```
#### Classes with `__call__` and `__new__`
@ -1266,6 +1280,123 @@ static_assert(is_subtype_of(TypeOf[F], Callable[[], int]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[], str]))
```
#### Classes with `__init__`
```py
from typing import Callable, overload
from ty_extensions import TypeOf, static_assert, is_subtype_of
class A:
def __init__(self, a: int) -> None: ...
static_assert(is_subtype_of(TypeOf[A], Callable[[int], A]))
static_assert(not is_subtype_of(TypeOf[A], Callable[[], A]))
class B:
@overload
def __init__(self, a: int) -> None: ...
@overload
def __init__(self) -> None: ...
def __init__(self, a: int | None = None) -> None: ...
static_assert(is_subtype_of(TypeOf[B], Callable[[int], B]))
static_assert(is_subtype_of(TypeOf[B], Callable[[], B]))
class C: ...
# TODO: This assertion should be true once we understand `Self`
# error: [static-assert-error] "Static assertion error: argument evaluates to `False`"
static_assert(is_subtype_of(TypeOf[C], Callable[[], C]))
class D[T]:
def __init__(self, x: T) -> None: ...
static_assert(is_subtype_of(TypeOf[D[int]], Callable[[int], D[int]]))
static_assert(not is_subtype_of(TypeOf[D[int]], Callable[[str], D[int]]))
```
#### Classes with `__init__` and `__new__`
```py
from typing import Callable, overload, Self
from ty_extensions import TypeOf, static_assert, is_subtype_of
class A:
def __new__(cls, a: int) -> Self:
return super().__new__(cls)
def __init__(self, a: int) -> None: ...
static_assert(is_subtype_of(TypeOf[A], Callable[[int], A]))
static_assert(not is_subtype_of(TypeOf[A], Callable[[], A]))
class B:
def __new__(cls, a: int) -> int:
return super().__new__(cls)
def __init__(self, a: str) -> None: ...
static_assert(is_subtype_of(TypeOf[B], Callable[[int], int]))
static_assert(not is_subtype_of(TypeOf[B], Callable[[str], B]))
class C:
def __new__(cls, *args, **kwargs) -> "C":
return super().__new__(cls)
def __init__(self, x: int) -> None: ...
# Not subtype because __new__ signature is not fully static
static_assert(not is_subtype_of(TypeOf[C], Callable[[int], C]))
static_assert(not is_subtype_of(TypeOf[C], Callable[[], C]))
class D: ...
class E:
@overload
def __new__(cls) -> int: ...
@overload
def __new__(cls, x: int) -> D: ...
def __new__(cls, x: int | None = None) -> int | D:
return D()
def __init__(self, y: str) -> None: ...
static_assert(is_subtype_of(TypeOf[E], Callable[[int], D]))
static_assert(is_subtype_of(TypeOf[E], Callable[[], int]))
class F[T]:
def __new__(cls, x: T) -> "F[T]":
return super().__new__(cls)
def __init__(self, x: T) -> None: ...
static_assert(is_subtype_of(TypeOf[F[int]], Callable[[int], F[int]]))
static_assert(not is_subtype_of(TypeOf[F[int]], Callable[[str], F[int]]))
```
#### Classes with `__call__`, `__new__` and `__init__`
If `__call__`, `__new__` and `__init__` are all present, `__call__` takes precedence.
```py
from typing import Callable
from ty_extensions import TypeOf, static_assert, is_subtype_of
class MetaWithIntReturn(type):
def __call__(cls) -> int:
return super().__call__()
class F(metaclass=MetaWithIntReturn):
def __new__(cls) -> str:
return super().__new__(cls)
def __init__(self, x: int) -> None: ...
static_assert(is_subtype_of(TypeOf[F], Callable[[], int]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[], str]))
static_assert(not is_subtype_of(TypeOf[F], Callable[[int], F]))
```
### Bound methods
```py