mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 02:38:25 +00:00
[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:
parent
16621fa19d
commit
c60b4d7f30
5 changed files with 329 additions and 46 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue