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:
@@ -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],
|
||||
): ...
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user