diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md new file mode 100644 index 00000000000000..e39472a989dd0a --- /dev/null +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md @@ -0,0 +1,170 @@ +# PEP 695 rules (`UP040`, `UP046`, `UP047`) + +```toml +target-version = "py313" + +[lint] +preview = true +select = [ + "non-pep695-type-alias", + "non-pep695-generic-class", + "non-pep695-generic-function", +] +``` + +## Defaulted `TypeVar` before a non-defaulted one + +In a PEP 695 type parameter list, a non-defaulted type parameter cannot follow a defaulted one, and +reordering the parameters would change how positional arguments bind when subscripting the alias, +class, or function. There is no valid, equivalent type parameter list in this case, so no diagnostic +is emitted (see [#27021](https://github.com/astral-sh/ruff/issues/27021)). + +### `non-pep695-type-alias` (`UP040`) + +#### No diagnostic for a `TypeAlias` annotation + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[T, S] +``` + +#### No diagnostic for a `TypeAliasType` call + +```py +from typing import TypeAliasType, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair = TypeAliasType("Pair", tuple[T, S], type_params=(T, S)) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +``` + +```snapshot +error[UP040]: Type alias `Pair` uses `TypeAlias` annotation instead of the `type` keyword + --> src/mdtest_snippet.py:6:1 + | +6 | Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | +help: Use the `type` keyword + | +5 | + - Pair: TypeAlias = tuple[S, T] # snapshot: non-pep695-type-alias +6 + type Pair[S, T = int] = tuple[S, T] # snapshot: non-pep695-type-alias + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### The fix is still offered when all of the type variables have defaults + +```py +from typing import TypeAlias, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S", default=str) + +Pair: TypeAlias = tuple[T, S] # error: [non-pep695-type-alias] +``` + +### `non-pep695-generic-class` (`UP046`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[T, S]): + t: T + s: S +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import Generic, TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + t: T + s: S +``` + +```snapshot +error[UP046]: Generic class `Pair` uses `Generic` subclass instead of type parameters + --> src/mdtest_snippet.py:6:12 + | +6 | class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class + | ^^^^^^^^^^^^^ + | +help: Use type parameters + | +5 | + - class Pair(Generic[S, T]): # snapshot: non-pep695-generic-class +6 + class Pair[S, T = int]: # snapshot: non-pep695-generic-class +7 | t: T + | +note: This is an unsafe fix and may change runtime behavior +``` + +### `non-pep695-generic-function` (`UP047`) + +#### No diagnostic when a defaulted `TypeVar` precedes a non-defaulted one + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(t: T, s: S) -> tuple[T, S]: + return (t, s) +``` + +#### The fix is still offered when the non-defaulted `TypeVar` comes first + +```py +from typing import TypeVar + +T = TypeVar("T", default=int) +S = TypeVar("S") + +def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + return (s, t) +``` + +```snapshot +error[UP047]: Generic function `pair` should use type parameters + --> src/mdtest_snippet.py:6:5 + | +6 | def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function + | ^^^^^^^^^^^^^^^^ + | +help: Use type parameters + | +5 | + - def pair(s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +6 + def pair[S, T = int](s: S, t: T) -> tuple[S, T]: # snapshot: non-pep695-generic-function +7 | return (s, t) + | +note: This is an unsafe fix and may change runtime behavior +``` 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 c2c6f00fdeb84d..109ccd762db48c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs @@ -367,6 +367,20 @@ fn in_nested_context(checker: &Checker) -> bool { .any(|stmt| matches!(stmt, Stmt::ClassDef(_) | Stmt::FunctionDef(_))) } +/// Returns `true` if a type variable without a default follows a type variable with a default. +/// +/// In a PEP 695 type parameter list this is a syntax error: +/// +/// ```python +/// type Pair[T = int, S] = tuple[T, S] # non-default type parameter `S` follows default type parameter +/// ``` +fn non_default_follows_default(type_vars: &[TypeVar]) -> bool { + type_vars + .iter() + .skip_while(|tv| tv.default.is_none()) + .any(|tv| tv.default.is_none()) +} + /// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found. /// Also returns `None` if any `TypeVar` has a default value and the target Python version /// is below 3.13 or preview mode is not enabled. Note that `typing_extensions` backports @@ -385,6 +399,10 @@ fn check_type_vars<'a>(vars: Vec>, checker: &Checker) -> Option