[ty] Use separate Rust types for bound and unbound type variables (#19796)

This PR creates separate Rust types for bound and unbound type
variables, as proposed in https://github.com/astral-sh/ty/issues/926.

Closes https://github.com/astral-sh/ty/issues/926

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-08-11 15:29:58 -04:00 committed by GitHub
parent f3f4db7104
commit dc84645c36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 846 additions and 560 deletions

View file

@ -102,7 +102,7 @@ def silence[T: type[BaseException]](
try:
func()
except exception_type as e:
reveal_type(e) # revealed: T'instance
reveal_type(e) # revealed: T'instance@silence
def silence2[T: (
type[ValueError],
@ -111,7 +111,7 @@ def silence2[T: (
try:
func()
except exception_type as e:
reveal_type(e) # revealed: T'instance
reveal_type(e) # revealed: T'instance@silence2
```
## Invalid exception handlers

View file

@ -432,3 +432,26 @@ def NamedTemporaryFile(suffix: T | None, prefix: T | None) -> None:
def f(x: str):
NamedTemporaryFile(prefix=x, suffix=".tar.gz") # Fine
```
## Nested functions see typevars bound in outer function
```py
from typing import TypeVar, overload
T = TypeVar("T")
def outer(t: T) -> None:
def inner(t: T) -> None: ...
inner(t)
@overload
def overloaded_outer() -> None: ...
@overload
def overloaded_outer(t: T) -> None: ...
def overloaded_outer(t: T | None = None) -> None:
def inner(t: T) -> None: ...
if t is not None:
inner(t)
```

View file

@ -20,7 +20,7 @@ from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: typing.TypeVar("T")
reveal_type(T.__name__) # revealed: Literal["T"]
```
@ -79,6 +79,8 @@ python-version = "3.13"
from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
@ -113,6 +115,8 @@ class Invalid(Generic[U]): ...
from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
@ -126,6 +130,8 @@ reveal_type(S.__bound__) # revealed: None
from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")

View file

@ -433,3 +433,24 @@ def f[T: (str, bytes)](suffix: T | None, prefix: T | None):
def g(x: str):
f(prefix=x, suffix=".tar.gz")
```
## Nested functions see typevars bound in outer function
```py
from typing import overload
def outer[T](t: T) -> None:
def inner[T](t: T) -> None: ...
inner(t)
@overload
def overloaded_outer() -> None: ...
@overload
def overloaded_outer[T](t: T) -> None: ...
def overloaded_outer[T](t: T | None = None) -> None:
def inner(t: T) -> None: ...
if t is not None:
inner(t)
```

View file

@ -17,7 +17,7 @@ instances of `typing.TypeVar`, just like legacy type variables.
```py
def f[T]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T) # revealed: typing.TypeVar("T")
reveal_type(T.__name__) # revealed: Literal["T"]
```
@ -32,6 +32,8 @@ python-version = "3.13"
```py
def f[T = int]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
@ -63,6 +65,8 @@ class Invalid[S = T]: ...
```py
def f[T: int]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
@ -74,6 +78,8 @@ def g[S]():
```py
def f[T: (int, str)]():
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
reveal_type(T.__bound__) # revealed: None

View file

@ -21,7 +21,9 @@ static_assert(is_subtype_of(Never, Never))
static_assert(not is_subtype_of(int, Never))
T = TypeVar("T", bound=Never)
static_assert(is_subtype_of(T, Never))
def _(t: T):
static_assert(is_subtype_of(T, Never))
```
## `Never` is assignable to every type