diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py index 6c82539345f7c..f7581f596823f 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py @@ -149,3 +149,10 @@ class Foo: | str ), ) + + +# A sole starred constraint needs a trailing comma in the PEP 695 constraint tuple. +constraints = (int, str) +T_starred = TypeVar("T_starred", *constraints) +StarredList: TypeAlias = list[T_starred] +StarredAlias = TypeAliasType("StarredAlias", list[T_starred], type_params=(T_starred,)) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py index 7983ed47560a4..2fb92e969c39c 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py @@ -142,3 +142,20 @@ class DefaultOnlyTypeVar(Generic[W]): # -> [W = int] class Outer: class Inner(Generic[T]): var: T + + +# A sole starred constraint needs a trailing comma in the PEP 695 constraint tuple. +constraints = (int, str) +U = TypeVar("U", *constraints) + + +class StarredConstraints(Generic[U]): + var: U + + +# Mixed constraints retain both the explicit and unpacked types. +V = TypeVar("V", bool, *constraints) + + +class MixedConstraints(Generic[V]): + var: V diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py index eba7ff15972c1..4e55541a18308 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py @@ -63,3 +63,12 @@ def multi_param(t: list[T], c: Callable[[T], None]) -> T: def outer(): def inner(t: T) -> T: return t + + +# A sole starred constraint needs a trailing comma in the PEP 695 constraint tuple. +constraints = (int, str) +U = TypeVar("U", *constraints) + + +def starred_constraints(var: U) -> U: + return var diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF053.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF053.py index 06b46a8b8a3f3..90a965f5848a5 100644 --- a/crates/ruff_linter/resources/test/fixtures/ruff/RUF053.py +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF053.py @@ -98,7 +98,16 @@ class C[T: [a]](Generic[_A]): ... # Existing bounds should not be deparenthesized. # class C[T: (_Y := int)](Generic[_A]): ... # TODO: Uncomment this -# class C[T: (*a,)](Generic[_A]): ... # TODO: Uncomment this + + +# A sole starred constraint needs a trailing comma in the PEP 695 constraint tuple. +constraints = (str, bytes) +_H = TypeVar('_H', *constraints) +class C[T](Generic[_H]): ... + + +# Existing starred constraint tuples retain their trailing comma. +class C[T: (*constraints,)](Generic[_A]): ... ### No errors diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs index 2155add5297f7..3edb9a055e6bd 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -119,15 +119,21 @@ impl Display for DisplayTypeVar<'_> { } TypeVarRestriction::AnyStr => f.write_str("(bytes, str)")?, TypeVarRestriction::Constraint(vec) => { - let len = vec.len(); - f.write_str("(")?; - for (i, v) in vec.iter().enumerate() { - f.write_str(&self.source[v.range()])?; - if i < len - 1 { - f.write_str(", ")?; + match vec.as_slice() { + // A single starred expression needs a comma to form a constraint tuple: + // `TypeVar("T", *constraints)` becomes `T: (*constraints,)`. + [element] => { + write!(f, "({element},)", element = &self.source[element.range()])?; + } + _ => { + write!( + f, + "({})", + vec.iter() + .format_with(", ", |expr, f| f(&&self.source[expr.range()])), + )?; } } - f.write_str(")")?; } } } diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap index 92581cfca6407..4d02146ad2c4e 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP040.py.snap @@ -497,5 +497,39 @@ help: Use the `type` keyword - ), - ) 148 + ) +149 | + | +note: This is an unsafe fix and may change runtime behavior + +UP040 [*] Type alias `StarredList` uses `TypeAlias` annotation instead of the `type` keyword + --> UP040.py:157:1 + | +155 | constraints = (int, str) +156 | T_starred = TypeVar("T_starred", *constraints) +157 | StarredList: TypeAlias = list[T_starred] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +158 | StarredAlias = TypeAliasType("StarredAlias", list[T_starred], type_params=(T_starred,)) + | +help: Use the `type` keyword + | +156 | T_starred = TypeVar("T_starred", *constraints) + - StarredList: TypeAlias = list[T_starred] +157 + type StarredList[T_starred: (*constraints,)] = list[T_starred] +158 | StarredAlias = TypeAliasType("StarredAlias", list[T_starred], type_params=(T_starred,)) + | +note: This is an unsafe fix and may change runtime behavior + +UP040 [*] Type alias `StarredAlias` uses `TypeAliasType` assignment instead of the `type` keyword + --> UP040.py:158:1 + | +156 | T_starred = TypeVar("T_starred", *constraints) +157 | StarredList: TypeAlias = list[T_starred] +158 | StarredAlias = TypeAliasType("StarredAlias", list[T_starred], type_params=(T_starred,)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Use the `type` keyword + | +157 | StarredList: TypeAlias = list[T_starred] + - StarredAlias = TypeAliasType("StarredAlias", list[T_starred], type_params=(T_starred,)) +158 + type StarredAlias[T_starred: (*constraints,)] = list[T_starred] | note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap index 0dcc52d70435b..e00db380b01db 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP046_0.py.snap @@ -253,3 +253,35 @@ UP046 Generic class `TooManyGenerics` uses `Generic` subclass instead of type pa 100 | var: S | help: Use type parameters + +UP046 [*] Generic class `StarredConstraints` uses `Generic` subclass instead of type parameters + --> UP046_0.py:152:26 + | +152 | class StarredConstraints(Generic[U]): + | ^^^^^^^^^^ +153 | var: U + | +help: Use type parameters + | +151 | + - class StarredConstraints(Generic[U]): +152 + class StarredConstraints[U: (*constraints,)]: +153 | var: U + | +note: This is an unsafe fix and may change runtime behavior + +UP046 [*] Generic class `MixedConstraints` uses `Generic` subclass instead of type parameters + --> UP046_0.py:160:24 + | +160 | class MixedConstraints(Generic[V]): + | ^^^^^^^^^^ +161 | var: V + | +help: Use type parameters + | +159 | + - class MixedConstraints(Generic[V]): +160 + class MixedConstraints[V: (bool, *constraints)]: +161 | var: V + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap index ab0cd50aeee44..b57cce4d4f29a 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP047_0.py.snap @@ -122,3 +122,19 @@ help: Use type parameters 57 | c(t[0]) | note: This is an unsafe fix and may change runtime behavior + +UP047 [*] Generic function `starred_constraints` should use type parameters + --> UP047_0.py:73:5 + | +73 | def starred_constraints(var: U) -> U: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +74 | return var + | +help: Use type parameters + | +72 | + - def starred_constraints(var: U) -> U: +73 + def starred_constraints[U: (*constraints,)](var: U) -> U: +74 | return var + | +note: This is an unsafe fix and may change runtime behavior diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class-with-mixed-type-vars_RUF053.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class-with-mixed-type-vars_RUF053.py.snap index ec5fcd2256cab..0f1809cd212f9 100644 --- a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class-with-mixed-type-vars_RUF053.py.snap +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__class-with-mixed-type-vars_RUF053.py.snap @@ -429,7 +429,7 @@ help: Remove `Generic` base class | 94 | # Single-element constraints should not be converted to a bound. - class C[T: (str,)](Generic[_A]): ... -95 + class C[T: (str), _A]: ... +95 + class C[T: (str,), _A]: ... 96 | class C[T: [a]](Generic[_A]): ... | note: This is an unsafe fix and may change runtime behavior @@ -449,3 +449,34 @@ help: Remove `Generic` base class 97 | | note: This is an unsafe fix and may change runtime behavior + +RUF053 [*] Class with type parameter list inherits from `Generic` + --> RUF053.py:106:12 + | +104 | constraints = (str, bytes) +105 | _H = TypeVar('_H', *constraints) +106 | class C[T](Generic[_H]): ... + | ^^^^^^^^^^^ +help: Remove `Generic` base class + | +105 | _H = TypeVar('_H', *constraints) + - class C[T](Generic[_H]): ... +106 + class C[T, _H: (*constraints,)]: ... +107 | + | +note: This is an unsafe fix and may change runtime behavior + +RUF053 [*] Class with type parameter list inherits from `Generic` + --> RUF053.py:110:29 + | +109 | # Existing starred constraint tuples retain their trailing comma. +110 | class C[T: (*constraints,)](Generic[_A]): ... + | ^^^^^^^^^^^ +help: Remove `Generic` base class + | +109 | # Existing starred constraint tuples retain their trailing comma. + - class C[T: (*constraints,)](Generic[_A]): ... +110 + class C[T: (*constraints,), _A]: ... +111 | + | +note: This is an unsafe fix and may change runtime behavior