Merge remote-tracking branch 'origin/main' into dcreager/callable-return

* origin/main:
  Fluent formatting of method chains (#21369)
  [ty] Avoid stack overflow when calculating inferable typevars (#21971)
  [ty] Add "qualify ..." code fix for undefined references (#21968)
  [ty] Use jemalloc on linux (#21975)
  Update MSRV to 1.90 (#21987)
  [ty] Improve check enforcing that an overloaded function must have an implementation (#21978)
  Update actions/checkout digest to 8e8c483 (#21982)
  [ty] Use `ParamSpec` without the attr for inferable check (#21934)
  [ty] Emit diagnostic when a type variable with a default is followed by one without a default (#21787)
This commit is contained in:
Douglas Creager
2025-12-15 11:06:49 -05:00
51 changed files with 2057 additions and 301 deletions

View File

@@ -0,0 +1,43 @@
# Invalid Order of Legacy Type Parameters
<!-- snapshot-diagnostics -->
```toml
[environment]
python-version = "3.13"
```
```py
from typing import TypeVar, Generic, Protocol
T1 = TypeVar("T1", default=int)
T2 = TypeVar("T2")
T3 = TypeVar("T3")
DefaultStrT = TypeVar("DefaultStrT", default=str)
class SubclassMe(Generic[T1, DefaultStrT]):
x: DefaultStrT
class Baz(SubclassMe[int, DefaultStrT]):
pass
# error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
class Foo(Generic[T1, T2]):
pass
class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
pass
class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
pass
class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
pass
class VeryBad(
Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
Generic[T1, T2, DefaultStrT, T3],
): ...
```

View File

@@ -424,9 +424,8 @@ p3 = ParamSpecWithDefault4[[int], [str]]()
reveal_type(p3.attr1) # revealed: (int, /) -> None
reveal_type(p3.attr2) # revealed: (str, /) -> None
# TODO: error
# Un-ordered type variables as the default of `PAnother` is `P`
class ParamSpecWithDefault5(Generic[PAnother, P]):
class ParamSpecWithDefault5(Generic[PAnother, P]): # error: [invalid-generic-class]
attr: Callable[PAnother, None]
# TODO: error

View File

@@ -800,6 +800,29 @@ def func(x: D): ...
func(G()) # error: [invalid-argument-type]
```
### Self-referential protocol with different specialization
This is a minimal reproduction for [ty#1874](https://github.com/astral-sh/ty/issues/1874).
```py
from __future__ import annotations
from typing import Protocol
from ty_extensions import generic_context
class A[S, R](Protocol):
def get(self, s: S) -> R: ...
def set(self, s: S, r: R) -> S: ...
def merge[R2](self, other: A[S, R2]) -> A[S, tuple[R, R2]]: ...
class Impl[S, R](A[S, R]):
def foo(self, s: S) -> S:
return self.set(s, self.get(s))
reveal_type(generic_context(A.get)) # revealed: ty_extensions.GenericContext[Self@get]
reveal_type(generic_context(A.merge)) # revealed: ty_extensions.GenericContext[Self@merge, R2@merge]
reveal_type(generic_context(Impl.foo)) # revealed: ty_extensions.GenericContext[Self@foo]
```
## Tuple as a PEP-695 generic class
Our special handling for `tuple` does not break if `tuple` is defined as a PEP-695 generic class in

View File

@@ -687,3 +687,59 @@ reveal_type(with_parameters(int_int, 1)) # revealed: Overload[(x: int) -> str,
# error: [invalid-argument-type]
reveal_type(with_parameters(int_int, "a")) # revealed: Overload[(x: int) -> str, (x: str) -> str]
```
## ParamSpec attribute assignability
When comparing signatures with `ParamSpec` attributes (`P.args` and `P.kwargs`), two different
inferable `ParamSpec` attributes with the same kind are assignable to each other. This enables
method overrides where both methods have their own `ParamSpec`.
### Same attribute kind, both inferable
```py
from typing import Callable
class Parent:
def method[**P](self, callback: Callable[P, None]) -> Callable[P, None]:
return callback
class Child1(Parent):
# This is a valid override: Q.args matches P.args, Q.kwargs matches P.kwargs
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
return callback
# Both signatures use ParamSpec, so they should be compatible
def outer[**P](f: Callable[P, int]) -> Callable[P, int]:
def inner[**Q](g: Callable[Q, int]) -> Callable[Q, int]:
return g
return inner(f)
```
We can explicitly mark it as an override using the `@override` decorator.
```py
from typing import override
class Child2(Parent):
@override
def method[**Q](self, callback: Callable[Q, None]) -> Callable[Q, None]:
return callback
```
### One `ParamSpec` not inferable
Here, `P` is in a non-inferable position while `Q` is inferable. So, they are not considered
assignable.
```py
from typing import Callable
class Container[**P]:
def method(self, f: Callable[P, None]) -> Callable[P, None]:
return f
def try_assign[**Q](self, f: Callable[Q, None]) -> Callable[Q, None]:
# error: [invalid-return-type] "Return type does not match returned value: expected `(**Q@try_assign) -> None`, found `(**P@Container) -> None`"
# error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
return self.method(f)
```

View File

@@ -418,6 +418,18 @@ Using the `@abstractmethod` decorator requires that the class's metaclass is `AB
from it.
```py
from abc import ABCMeta
class CustomAbstractMetaclass(ABCMeta): ...
class Fine(metaclass=CustomAbstractMetaclass):
@overload
@abstractmethod
def f(self, x: int) -> int: ...
@overload
@abstractmethod
def f(self, x: str) -> str: ...
class Foo:
@overload
@abstractmethod
@@ -448,6 +460,52 @@ class PartialFoo(ABC):
def f(self, x: str) -> str: ...
```
#### `TYPE_CHECKING` blocks
As in other areas of ty, we treat `TYPE_CHECKING` blocks the same as "inline stub files", so we
permit overloaded functions to exist without an implementation if all overloads are defined inside
an `if TYPE_CHECKING` block:
```py
from typing import overload, TYPE_CHECKING
if TYPE_CHECKING:
@overload
def a() -> str: ...
@overload
def a(x: int) -> int: ...
class F:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
class G:
if TYPE_CHECKING:
@overload
def method(self) -> None: ...
@overload
def method(self, x: int) -> int: ...
if TYPE_CHECKING:
@overload
def b() -> str: ...
if TYPE_CHECKING:
@overload
def b(x: int) -> int: ...
if TYPE_CHECKING:
@overload
def c() -> None: ...
# not all overloads are in a `TYPE_CHECKING` block, so this is an error
@overload
# error: [invalid-overload]
def c(x: int) -> int: ...
```
### `@overload`-decorated functions with non-stub bodies
<!-- snapshot-diagnostics -->

View File

@@ -0,0 +1,190 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: invalid_type_parameter_order.md - Invalid Order of Legacy Type Parameters
mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/invalid_type_parameter_order.md
---
# Python source files
## mdtest_snippet.py
```
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
4 |
5 | T2 = TypeVar("T2")
6 | T3 = TypeVar("T3")
7 |
8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
9 |
10 | class SubclassMe(Generic[T1, DefaultStrT]):
11 | x: DefaultStrT
12 |
13 | class Baz(SubclassMe[int, DefaultStrT]):
14 | pass
15 |
16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
17 | class Foo(Generic[T1, T2]):
18 | pass
19 |
20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
21 | pass
22 |
23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
24 | pass
25 |
26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
27 | pass
28 |
29 | class VeryBad(
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
31 | Generic[T1, T2, DefaultStrT, T3],
32 | ): ...
```
# Diagnostics
```
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
--> src/mdtest_snippet.py:17:19
|
16 | # error: [invalid-generic-class] "Type parameter `T2` without a default cannot follow earlier parameter `T1` with a default"
17 | class Foo(Generic[T1, T2]):
| ^^^^^^
| |
| Type variable `T2` does not have a default
| Earlier TypeVar `T1` does
18 | pass
|
::: src/mdtest_snippet.py:3:1
|
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
| ------------------------------- `T1` defined here
4 |
5 | T2 = TypeVar("T2")
| ------------------ `T2` defined here
6 | T3 = TypeVar("T3")
|
info: rule `invalid-generic-class` is enabled by default
```
```
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
--> src/mdtest_snippet.py:20:19
|
18 | pass
19 |
20 | class Bar(Generic[T2, T1, T3]): # error: [invalid-generic-class]
| ^^^^^^^^^^
| |
| Type variable `T3` does not have a default
| Earlier TypeVar `T1` does
21 | pass
|
::: src/mdtest_snippet.py:3:1
|
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
| ------------------------------- `T1` defined here
4 |
5 | T2 = TypeVar("T2")
6 | T3 = TypeVar("T3")
| ------------------ `T3` defined here
7 |
8 | DefaultStrT = TypeVar("DefaultStrT", default=str)
|
info: rule `invalid-generic-class` is enabled by default
```
```
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
--> src/mdtest_snippet.py:23:20
|
21 | pass
22 |
23 | class Spam(Generic[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| Type variables `T2` and `T3` do not have defaults
| Earlier TypeVar `T1` does
24 | pass
|
::: src/mdtest_snippet.py:3:1
|
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
| ------------------------------- `T1` defined here
4 |
5 | T2 = TypeVar("T2")
| ------------------ `T2` defined here
6 | T3 = TypeVar("T3")
|
info: rule `invalid-generic-class` is enabled by default
```
```
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
--> src/mdtest_snippet.py:26:20
|
24 | pass
25 |
26 | class Ham(Protocol[T1, T2, DefaultStrT, T3]): # error: [invalid-generic-class]
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| Type variables `T2` and `T3` do not have defaults
| Earlier TypeVar `T1` does
27 | pass
|
::: src/mdtest_snippet.py:3:1
|
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
| ------------------------------- `T1` defined here
4 |
5 | T2 = TypeVar("T2")
| ------------------ `T2` defined here
6 | T3 = TypeVar("T3")
|
info: rule `invalid-generic-class` is enabled by default
```
```
error[invalid-generic-class]: Type parameters without defaults cannot follow type parameters with defaults
--> src/mdtest_snippet.py:30:14
|
29 | class VeryBad(
30 | Protocol[T1, T2, DefaultStrT, T3], # error: [invalid-generic-class]
| ^^^^^^^^^^^^^^^^^^^^^^^
| |
| Type variables `T2` and `T3` do not have defaults
| Earlier TypeVar `T1` does
31 | Generic[T1, T2, DefaultStrT, T3],
32 | ): ...
|
::: src/mdtest_snippet.py:3:1
|
1 | from typing import TypeVar, Generic, Protocol
2 |
3 | T1 = TypeVar("T1", default=int)
| ------------------------------- `T1` defined here
4 |
5 | T2 = TypeVar("T2")
| ------------------ `T2` defined here
6 | T3 = TypeVar("T3")
|
info: rule `invalid-generic-class` is enabled by default
```

View File

@@ -42,7 +42,11 @@ error[invalid-overload]: Overloads for function `func` must be followed by a non
9 | class Foo:
|
info: Attempting to call `func` will raise `TypeError` at runtime
info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
info: Overloaded functions without implementations are only permitted:
info: - in stub files
info: - in `if TYPE_CHECKING` blocks
info: - as methods on protocol classes
info: - or as `@abstractmethod`-decorated methods on abstract classes
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
info: rule `invalid-overload` is enabled by default
@@ -58,7 +62,11 @@ error[invalid-overload]: Overloads for function `method` must be followed by a n
| ^^^^^^
|
info: Attempting to call `method` will raise `TypeError` at runtime
info: Overloaded functions without implementations are only permitted in stub files, on protocols, or for abstract methods
info: Overloaded functions without implementations are only permitted:
info: - in stub files
info: - in `if TYPE_CHECKING` blocks
info: - as methods on protocol classes
info: - or as `@abstractmethod`-decorated methods on abstract classes
info: See https://docs.python.org/3/library/typing.html#typing.overload for more details
info: rule `invalid-overload` is enabled by default