Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 20 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP040.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,23 @@ class Foo:
# Test case for TypeVar with default - should be converted when preview mode is enabled
T_default = TypeVar("T_default", default=int)
DefaultList: TypeAlias = list[T_default]

# https://github.com/astral-sh/ruff/issues/27021
# No fix if a defaulted TypeVar precedes a non-defaulted TypeVar: a non-default
# type parameter can't follow a default type parameter in a PEP 695 type
# parameter list, and reordering would change the meaning of subscriptions.
T_with_default = TypeVar("T_with_default", default=int)
S_no_default = TypeVar("S_no_default")
BadPair: TypeAlias = tuple[T_with_default, S_no_default]
BadPairAliasType = TypeAliasType(
"BadPairAliasType",
tuple[T_with_default, S_no_default],
type_params=(T_with_default, S_no_default),
)

# OK (in preview): the non-defaulted TypeVar comes first
GoodPair: TypeAlias = tuple[S_no_default, T_with_default]

# OK (in preview): all TypeVars have defaults
S_with_default = TypeVar("S_with_default", default=str)
AllDefaults: TypeAlias = tuple[T_with_default, S_with_default]
Comment thread
Jayashanker-Padishala marked this conversation as resolved.
Outdated
19 changes: 19 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP046_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,22 @@ class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
class Outer:
class Inner(Generic[T]):
var: T


# https://github.com/astral-sh/ruff/issues/27021
# No diagnostic if a defaulted TypeVar precedes a non-defaulted TypeVar: a
# non-default type parameter can't follow a default type parameter in a PEP 695
# type parameter list, and reordering would change the meaning of subscriptions.
X = TypeVar("X", default=int)
Y = TypeVar("Y")


class DefaultBeforeNonDefault(Generic[X, Y]):
x: X
y: Y


# OK (in preview): the non-defaulted TypeVar comes first
class NonDefaultBeforeDefault(Generic[Y, X]):
x: X
y: Y
17 changes: 17 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyupgrade/UP047_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ def multi_param(t: list[T], c: Callable[[T], None]) -> T:
return t[1]


# https://github.com/astral-sh/ruff/issues/27021
# No diagnostic if a defaulted TypeVar precedes a non-defaulted TypeVar: a
# non-default type parameter can't follow a default type parameter in a PEP 695
# type parameter list, and reordering would change the meaning of subscriptions.
X = TypeVar("X", default=int)
Y = TypeVar("Y")


def default_before_non_default(x: X, y: Y) -> tuple[X, Y]:
return (x, y)


# OK (in preview): the non-defaulted TypeVar comes first
def non_default_before_default(y: Y, x: X) -> tuple[Y, X]:
return (y, x)


# these cases are not handled

def outer():
Expand Down
25 changes: 25 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,24 @@ 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
/// ```
///
/// Reordering the parameters to make the list valid would not be an equivalent fix because the
/// parameter order determines how positional arguments are bound when subscripting the alias,
/// class, or function.
Comment thread
Jayashanker-Padishala marked this conversation as resolved.
Outdated
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 +403,13 @@ fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<
return None;
}

// If a type variable without a default follows one with a default, there is no valid,
// equivalent PEP 695 type parameter list, so bail out instead of emitting an invalid fix.
// See https://github.com/astral-sh/ruff/issues/27021.
Comment thread
Jayashanker-Padishala marked this conversation as resolved.
Outdated
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,13 @@ fn create_diagnostic(
return;
}

// If a type variable without a default follows one with a default, there is no valid,
// equivalent PEP 695 type parameter list, so bail out instead of emitting an invalid fix.
// See https://github.com/astral-sh/ruff/issues/27021.
Comment thread
Jayashanker-Padishala marked this conversation as resolved.
Outdated
if non_default_follows_default(type_vars) {
return;
}

let source = checker.source();
let tokens = checker.tokens();
let comment_ranges = checker.comment_ranges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs

--- Summary ---
Removed: 0
Added: 3
Added: 5

--- Added ---
UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
Expand Down Expand Up @@ -56,11 +56,50 @@ UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `t
133 | T_default = TypeVar("T_default", default=int)
134 | DefaultList: TypeAlias = list[T_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
135 |
136 | # https://github.com/astral-sh/ruff/issues/27021
|
help: Use the `type` keyword
|
133 | T_default = TypeVar("T_default", default=int)
- DefaultList: TypeAlias = list[T_default]
134 + type DefaultList[T_default = int] = list[T_default]
135 |
|
note: This is an unsafe fix and may change runtime behavior


UP040 [*] Type alias `GoodPair` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:150:1
|
149 | # OK (in preview): the non-defaulted TypeVar comes first
150 | GoodPair: TypeAlias = tuple[S_no_default, T_with_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
151 |
152 | # OK (in preview): all TypeVars have defaults
|
help: Use the `type` keyword
|
149 | # OK (in preview): the non-defaulted TypeVar comes first
- GoodPair: TypeAlias = tuple[S_no_default, T_with_default]
150 + type GoodPair[S_no_default, T_with_default = int] = tuple[S_no_default, T_with_default]
151 |
|
note: This is an unsafe fix and may change runtime behavior


UP040 [*] Type alias `AllDefaults` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:154:1
|
152 | # OK (in preview): all TypeVars have defaults
153 | S_with_default = TypeVar("S_with_default", default=str)
154 | AllDefaults: TypeAlias = tuple[T_with_default, S_with_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use the `type` keyword
|
153 | S_with_default = TypeVar("S_with_default", default=str)
- AllDefaults: TypeAlias = tuple[T_with_default, S_with_default]
154 + type AllDefaults[T_with_default = int, S_with_default = str] = tuple[T_with_default, S_with_default]
|
note: This is an unsafe fix and may change runtime behavior
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs

--- Summary ---
Removed: 0
Added: 2
Added: 3

--- Added ---
UP046 [*] Generic class `DefaultTypeVar` uses `Generic` subclass instead of type parameters
Expand Down Expand Up @@ -42,3 +42,22 @@ help: Use type parameters
138 | var: W
|
note: This is an unsafe fix and may change runtime behavior


UP046 [*] Generic class `NonDefaultBeforeDefault` uses `Generic` subclass instead of type parameters
--> UP046_0.py:161:31
|
160 | # OK (in preview): the non-defaulted TypeVar comes first
161 | class NonDefaultBeforeDefault(Generic[Y, X]):
| ^^^^^^^^^^^^^
162 | x: X
163 | y: Y
|
help: Use type parameters
|
160 | # OK (in preview): the non-defaulted TypeVar comes first
- class NonDefaultBeforeDefault(Generic[Y, X]):
161 + class NonDefaultBeforeDefault[Y, X = int]:
162 | x: X
|
note: This is an unsafe fix and may change runtime behavior
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs

--- Summary ---
Removed: 0
Added: 1
Added: 2

--- Added ---
UP047 [*] Generic function `default_var` should use type parameters
Expand All @@ -25,3 +25,21 @@ help: Use type parameters
52 | return v
|
note: This is an unsafe fix and may change runtime behavior


UP047 [*] Generic function `non_default_before_default` should use type parameters
--> UP047_0.py:74:5
|
73 | # OK (in preview): the non-defaulted TypeVar comes first
74 | def non_default_before_default(y: Y, x: X) -> tuple[Y, X]:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
75 | return (y, x)
|
help: Use type parameters
|
73 | # OK (in preview): the non-defaulted TypeVar comes first
- def non_default_before_default(y: Y, x: X) -> tuple[Y, X]:
74 + def non_default_before_default[Y, X = int](y: Y, x: X) -> tuple[Y, X]:
75 | return (y, x)
|
note: This is an unsafe fix and may change runtime behavior
Loading