[ty] Handle various invalid explicit specializations for ParamSpec (#21821)

## Summary

fixes: https://github.com/astral-sh/ty/issues/1788

## Test Plan

Add new mdtests.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Dhruv Manilawala
2025-12-08 10:50:41 +05:30
committed by GitHub
parent 857fd4f683
commit ac882f7e63
3 changed files with 65 additions and 13 deletions

View File

@@ -244,6 +244,7 @@ Explicit specialization of a generic class involving `ParamSpec` is done by prov
of types, `...`, or another in-scope `ParamSpec`.
```py
reveal_type(OnlyParamSpec[[]]().attr) # revealed: () -> None
reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None
reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None
@@ -252,8 +253,28 @@ def func(c: Callable[P2, None]):
# TODO: error: paramspec is unbound
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None
# error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`"
reveal_type(OnlyParamSpec[()]().attr) # revealed: (...) -> None
```
An explicit tuple expression (unlike an implicit one that omits the parentheses) is also accepted
when the `ParamSpec` is the only type variable. But, this isn't recommended is mainly a fallout of
it having the same AST as the one without the parentheses. Both mypy and Pyright also allow this.
```py
reveal_type(OnlyParamSpec[(int, str)]().attr) # revealed: (int, str, /) -> None
```
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
reveal_type(OnlyParamSpec[]().attr) # revealed: (...) -> None
```
<!-- blacken-docs:on -->
The square brackets can be omitted when `ParamSpec` is the only type variable
```py
@@ -269,6 +290,7 @@ reveal_type(OnlyParamSpec[int]().attr) # revealed: (int, /) -> None
But, they cannot be omitted when there are multiple type variables.
```py
reveal_type(TypeVarAndParamSpec[int, []]().attr) # revealed: () -> int
reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int
reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int
reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int
@@ -276,8 +298,12 @@ reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int
# TODO: We could still specialize for `T1` as the type is valid which would reveal `(...) -> int`
# TODO: error: paramspec is unbound
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be either a list of types, `ParamSpec`, `Concatenate`, or `...`"
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown
```
Nor can they be omitted when there are more than one `ParamSpec`s.

View File

@@ -228,6 +228,7 @@ Explicit specialization of a generic class involving `ParamSpec` is done by prov
of types, `...`, or another in-scope `ParamSpec`.
```py
reveal_type(OnlyParamSpec[[]]().attr) # revealed: () -> None
reveal_type(OnlyParamSpec[[int, str]]().attr) # revealed: (int, str, /) -> None
reveal_type(OnlyParamSpec[...]().attr) # revealed: (...) -> None
@@ -238,8 +239,28 @@ P2 = ParamSpec("P2")
# TODO: error: paramspec is unbound
reveal_type(OnlyParamSpec[P2]().attr) # revealed: (...) -> None
# error: [invalid-type-arguments] "No type argument provided for required type variable `P1` of class `OnlyParamSpec`"
reveal_type(OnlyParamSpec[()]().attr) # revealed: (...) -> None
```
An explicit tuple expression (unlike an implicit one that omits the parentheses) is also accepted
when the `ParamSpec` is the only type variable. But, this isn't recommended is mainly a fallout of
it having the same AST as the one without the parentheses. Both mypy and Pyright also allow this.
```py
reveal_type(OnlyParamSpec[(int, str)]().attr) # revealed: (int, str, /) -> None
```
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
reveal_type(OnlyParamSpec[]().attr) # revealed: (...) -> None
```
<!-- blacken-docs:on -->
The square brackets can be omitted when `ParamSpec` is the only type variable
```py
@@ -255,14 +276,19 @@ reveal_type(OnlyParamSpec[int]().attr) # revealed: (int, /) -> None
But, they cannot be omitted when there are multiple type variables.
```py
reveal_type(TypeVarAndParamSpec[int, []]().attr) # revealed: () -> int
reveal_type(TypeVarAndParamSpec[int, [int, str]]().attr) # revealed: (int, str, /) -> int
reveal_type(TypeVarAndParamSpec[int, [str]]().attr) # revealed: (str, /) -> int
reveal_type(TypeVarAndParamSpec[int, ...]().attr) # revealed: (...) -> int
# TODO: error: paramspec is unbound
reveal_type(TypeVarAndParamSpec[int, P2]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments]
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, int]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, ()]().attr) # revealed: (...) -> Unknown
# error: [invalid-type-arguments] "Type argument for `ParamSpec` must be"
reveal_type(TypeVarAndParamSpec[int, (int, str)]().attr) # revealed: (...) -> Unknown
```
Nor can they be omitted when there are more than one `ParamSpec`.