Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions crates/ruff_linter/resources/mdtest/pyupgrade/pep-695.md
Original file line number Diff line number Diff line change
@@ -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
```
18 changes: 18 additions & 0 deletions crates/ruff_linter/src/rules/pyupgrade/rules/pep695/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -385,6 +399,10 @@ fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<
return None;
}

if non_default_follows_default(&vars) {
return None;
}

// If any type variables were not unique, just bail out here. this is a runtime error and we
// can't predict what the user wanted.
(vars.iter().unique_by(|tvar| tvar.name).count() == vars.len()).then_some(vars)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use ruff_python_ast::PythonVersion;

use super::{
DisplayTypeVars, TypeParamKind, TypeVar, TypeVarReferenceVisitor, expr_name_to_type_var,
non_default_follows_default,
};

/// ## What it does
Expand Down Expand Up @@ -266,6 +267,10 @@ fn create_diagnostic(
return;
}

if non_default_follows_default(type_vars) {
return;
}

let source = checker.source();
let tokens = checker.tokens();
let comment_ranges = checker.comment_ranges();
Expand Down
Loading