mirror of
https://github.com/astral-sh/ruff.git
synced 2026-10-28 10:45:37 +08:00
[pyupgrade] Skip TypeVarTuple and ParamSpec conversions with bounds or constraints (UP040, UP046, UP047) (#28505)
Summary
--
The main 3.15-related change here is that `TypeVarTuple` now supports
the `bound` argument, which
was previously only allowed for `TypeVar`s (and `ParamSpec`s, see
below):
```py
from typing import TypeVar, TypeVarTuple
T = TypeVar("T", bound=int)
Ts = TypeVarTuple("Ts", bound=int) # new in 3.15
```
But PEP 695 doesn't include syntax for these bounds:
```py
def foo[T: int](): ... # valid TypeVar version
def foo[*Ts: int](): ... # analogous but invalid TypeVarTuple syntax
```
This was actually a preexisting bug, where we'd introduce a syntax error
if someone included an
invalid `bound` on their `TypeVarTuple`:
https://play.ruff.rs/f984fc13-8936-446c-bfde-35be960a448d
but this is a bigger issue on 3.15 now that `bound` is a supported
argument without corresponding
PEP-695 syntax.
While reviewing this, I realized that the same issue applies to
`ParamSpec`, which has actually
supported `bound` since 3.10, but again, there is no analogous PEP-695
syntax:
```py
def foo[**P: int](): ... # analogous but also invalid
```
The same issues also apply to constraints, thus the new
`has_unsupported_restriction` applies to both `TypeVarTuple` and
`ParamSpec` with either bounds or constraints.
Test Plan
--
New mdtests for the affected rules
This commit is contained in:
1 parent
a65f3d6a54
commit
677be7d4ef
3 files changed
+67
No files matched your search
@@ -165,3 +165,54 @@ help: Use type parameters
|
||||
|
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
```
|
||||
|
||||
## Bounded `TypeVarTuple`
|
||||
|
||||
Python 3.15 adds a `bound` argument to `TypeVarTuple`, but PEP 695 has no syntax to express it. These
|
||||
rules emit no diagnostic for type aliases, classes, or functions that use a bounded `TypeVarTuple`.
|
||||
|
||||
```toml
|
||||
target-version = "py315"
|
||||
|
||||
[lint]
|
||||
select = ["UP040", "UP046", "UP047"]
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeAlias, TypeAliasType, TypeVarTuple
|
||||
|
||||
Ts = TypeVarTuple("Ts", bound=int)
|
||||
|
||||
BoundedAlias: TypeAlias = tuple[*Ts] # no diagnostic
|
||||
BoundedAliasType = TypeAliasType("BoundedAliasType", tuple[*Ts], type_params=(Ts,)) # no diagnostic
|
||||
|
||||
class Bounded(Generic[*Ts]): ... # no diagnostic
|
||||
|
||||
def bounded(ts: tuple[*Ts]): ... # no diagnostic
|
||||
```
|
||||
|
||||
## Bounded `ParamSpec`
|
||||
|
||||
`ParamSpec` accepts a `bound` argument starting in Python 3.10, but PEP 695 has no syntax to express it.
|
||||
These rules emit no diagnostic for type aliases, classes, or functions that use a bounded `ParamSpec`.
|
||||
|
||||
```toml
|
||||
target-version = "py312"
|
||||
|
||||
[lint]
|
||||
select = ["UP040", "UP046", "UP047"]
|
||||
```
|
||||
|
||||
```py
|
||||
from collections.abc import Callable
|
||||
from typing import Generic, ParamSpec, TypeAlias, TypeAliasType
|
||||
|
||||
P = ParamSpec("P", bound=int)
|
||||
|
||||
BoundedAlias: TypeAlias = Callable[P, int] # no diagnostic
|
||||
BoundedAliasType = TypeAliasType("BoundedAliasType", Callable[P, int], type_params=(P,)) # no diagnostic
|
||||
|
||||
class Bounded(Generic[P]): ... # no diagnostic
|
||||
|
||||
def bounded(callback: Callable[P, int]): ... # no diagnostic
|
||||
```
|
||||
@@ -87,6 +87,14 @@ pub(crate) struct DisplayTypeVar<'a> {
|
||||
}
|
||||
|
||||
impl TypeVar<'_> {
|
||||
/// PEP 695 has no syntax for bounds or constraints on `TypeVarTuple` or `ParamSpec`.
|
||||
fn has_unsupported_restriction(&self) -> bool {
|
||||
matches!(
|
||||
self.kind,
|
||||
TypeParamKind::TypeVarTuple | TypeParamKind::ParamSpec
|
||||
) && self.restriction.is_some()
|
||||
}
|
||||
|
||||
fn display<'a>(&'a self, source: &'a str) -> DisplayTypeVar<'a> {
|
||||
DisplayTypeVar {
|
||||
type_var: self,
|
||||
@@ -390,6 +398,10 @@ fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<
|
||||
return None;
|
||||
}
|
||||
|
||||
if vars.iter().any(TypeVar::has_unsupported_restriction) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If any type variables have defaults, skip the rule unless
|
||||
// running with preview mode enabled and targeting Python 3.13+.
|
||||
if vars.iter().any(|tv| tv.default.is_some())
|
||||
|
||||
@@ -259,6 +259,10 @@ fn create_diagnostic(
|
||||
type_vars: &[TypeVar],
|
||||
type_alias_kind: TypeAliasKind,
|
||||
) {
|
||||
if type_vars.iter().any(TypeVar::has_unsupported_restriction) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If any type variables have defaults, skip the rule unless
|
||||
// running with preview mode enabled and targeting Python 3.13+.
|
||||
if (checker.target_version() < PythonVersion::PY313
|
||||
|
||||
Reference in new issue
Block a user