[ty] Promote literals when inferring class specializations from constructors (#18102)

This implements the stopgap approach described in
https://github.com/astral-sh/ty/issues/336#issuecomment-2880532213 for
handling literal types in generic class specializations.

With this approach, we will promote any literal to its instance type,
but _only_ when inferring a generic class specialization from a
constructor call:

```py
class C[T]:
    def __init__(self, x: T) -> None: ...

reveal_type(C("string"))  # revealed: C[str]
```

If you specialize the class explicitly, we still use whatever type you
provide, even if it's a literal:

```py
from typing import Literal

reveal_type(C[Literal[5]](5))  # revealed: C[Literal[5]]
```

And this doesn't apply at all to generic functions:

```py
def f[T](x: T) -> T:
    return x

reveal_type(f(5))  # revealed: Literal[5]
```

---

As part of making this happen, we also generalize the `TypeMapping`
machinery. This provides a way to apply a function to type, returning a
new type. Complicating matters is that for function literals, we have to
apply the mapping lazily, since the function's signature is not created
until (and if) someone calls its `signature` method. That means we have
to stash away the mappings that we want to apply to the signatures
parameter/return annotations once we do create it. This requires some
minor `Cow` shenanigans to continue working for partial specializations.
This commit is contained in:
Douglas Creager 2025-05-19 15:42:54 -04:00 committed by GitHub
parent fb589730ef
commit ce43dbab58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 215 additions and 176 deletions

View file

@ -90,7 +90,7 @@ reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTyp
The type parameter can be specified explicitly:
```py
from typing import Generic, TypeVar
from typing import Generic, Literal, TypeVar
T = TypeVar("T")
@ -98,6 +98,7 @@ class C(Generic[T]):
x: T
reveal_type(C[int]()) # revealed: C[int]
reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
```
The specialization must match the generic types:
@ -229,9 +230,9 @@ class C(Generic[T]):
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -245,9 +246,9 @@ T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -264,9 +265,9 @@ class C(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -283,9 +284,9 @@ class C(Generic[T]):
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D(Generic[T]):
@ -294,9 +295,9 @@ class D(Generic[T]):
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
reveal_type(D(1)) # revealed: D[int]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
@ -319,7 +320,7 @@ class C(Generic[T, U]):
class D(C[V, int]):
def __init__(self, x: V) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
reveal_type(D(1)) # revealed: D[int]
```
### `__init__` is itself generic
@ -333,11 +334,11 @@ T = TypeVar("T")
class C(Generic[T]):
def __init__(self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
reveal_type(C(1, 1)) # revealed: C[int]
reveal_type(C(1, "string")) # revealed: C[int]
reveal_type(C(1, True)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```

View file

@ -74,10 +74,13 @@ class BothGenericSyntaxes[U](Generic[T]): ...
The type parameter can be specified explicitly:
```py
from typing import Literal
class C[T]:
x: T
reveal_type(C[int]()) # revealed: C[int]
reveal_type(C[Literal[5]]()) # revealed: C[Literal[5]]
```
The specialization must match the generic types:
@ -190,9 +193,9 @@ class C[T]:
def __new__(cls, x: T) -> "C[T]":
return object.__new__(cls)
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -202,9 +205,9 @@ wrong_innards: C[int] = C("five")
class C[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -217,9 +220,9 @@ class C[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
```
@ -232,9 +235,9 @@ class C[T]:
def __init__(self, x: T) -> None: ...
reveal_type(C(1)) # revealed: C[Literal[1]]
reveal_type(C(1)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five")
class D[T]:
@ -243,9 +246,9 @@ class D[T]:
def __init__(self, *args, **kwargs) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
reveal_type(D(1)) # revealed: D[int]
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
# error: [invalid-assignment] "Object of type `D[str]` is not assignable to `D[int]`"
wrong_innards: D[int] = D("five")
```
@ -262,7 +265,7 @@ class C[T, U]:
class D[V](C[V, int]):
def __init__(self, x: V) -> None: ...
reveal_type(D(1)) # revealed: D[Literal[1]]
reveal_type(D(1)) # revealed: D[int]
```
### `__init__` is itself generic
@ -271,11 +274,11 @@ reveal_type(D(1)) # revealed: D[Literal[1]]
class C[T]:
def __init__[S](self, x: T, y: S) -> None: ...
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
reveal_type(C(1, True)) # revealed: C[Literal[1]]
reveal_type(C(1, 1)) # revealed: C[int]
reveal_type(C(1, "string")) # revealed: C[int]
reveal_type(C(1, True)) # revealed: C[int]
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
# error: [invalid-assignment] "Object of type `C[str]` is not assignable to `C[int]`"
wrong_innards: C[int] = C("five", 1)
```