Skip to content

Commit 1c161d3

Browse files
committed
Avoid applying PEP 646 rewrites in invalid contexts
1 parent 1279c20 commit 1c161d3

File tree

4 files changed

+177
-94
lines changed

4 files changed

+177
-94
lines changed
Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,58 @@
11
from typing import Generic, TypeVarTuple, Unpack
22

3-
Shape = TypeVarTuple('Shape')
3+
Shape = TypeVarTuple("Shape")
4+
45

56
class C(Generic[Unpack[Shape]]):
67
pass
78

8-
class D(Generic[Unpack [Shape]]):
9+
10+
class D(Generic[Unpack[Shape]]):
911
pass
1012

11-
def f(*args: Unpack[tuple[int, ...]]): pass
1213

13-
def f(*args: Unpack[other.Type]): pass
14+
def f(*args: Unpack[tuple[int, ...]]):
15+
pass
1416

1517

16-
# Not valid unpackings but they are valid syntax
17-
def foo(*args: Unpack[int | str]) -> None: pass
18-
def foo(*args: Unpack[int and str]) -> None: pass
19-
def foo(*args: Unpack[int > str]) -> None: pass
18+
def f(*args: Unpack[other.Type]):
19+
pass
20+
21+
22+
def f(*args: Generic[int, Unpack[int]]):
23+
pass
24+
25+
26+
# Valid syntax, but can't be unpacked.
27+
def f(*args: Unpack[int | str]) -> None:
28+
pass
29+
30+
31+
def f(*args: Unpack[int and str]) -> None:
32+
pass
33+
34+
35+
def f(*args: Unpack[int > str]) -> None:
36+
pass
37+
2038

21-
# We do not use the shorthand unpacking syntax in the following cases
2239
from typing import TypedDict
40+
41+
2342
class KwargsDict(TypedDict):
2443
x: int
2544
y: int
2645

27-
def foo(name: str, /, **kwargs: Unpack[KwargsDict]) -> None: pass
46+
47+
# OK
48+
def f(name: str, /, **kwargs: Unpack[KwargsDict]) -> None:
49+
pass
50+
51+
52+
# OK
53+
def f() -> object:
54+
return Unpack[tuple[int, ...]]
55+
56+
57+
# OK
58+
def f(x: Unpack[int]) -> object: ...

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
145145
if checker.enabled(Rule::FStringNumberFormat) {
146146
refurb::rules::fstring_number_format(checker, subscript);
147147
}
148-
149148
if checker.enabled(Rule::IncorrectlyParenthesizedTupleInSubscript) {
150149
ruff::rules::subscript_with_parenthesized_tuple(checker, subscript);
151150
}
152-
153151
if checker.enabled(Rule::NonPEP646Unpack) {
154152
pyupgrade::rules::use_pep646_unpack(checker, subscript);
155153
}

crates/ruff_linter/src/rules/pyupgrade/rules/use_pep646_unpack.rs

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
22
use ruff_macros::{derive_message_formats, violation};
33
use ruff_python_ast::ExprSubscript;
4+
use ruff_python_semantic::SemanticModel;
45

56
use crate::{checkers::ast::Checker, settings::types::PythonVersion};
67

@@ -14,7 +15,6 @@ use crate::{checkers::ast::Checker, settings::types::PythonVersion};
1415
/// `Unpack[]` syntax.
1516
///
1617
/// ## Example
17-
///
1818
/// ```python
1919
/// from typing import Unpack
2020
///
@@ -24,12 +24,16 @@ use crate::{checkers::ast::Checker, settings::types::PythonVersion};
2424
/// ```
2525
///
2626
/// Use instead:
27-
///
2827
/// ```python
2928
/// def foo(*args: *tuple[int, ...]) -> None:
3029
/// pass
3130
/// ```
3231
///
32+
/// ## Fix safety
33+
/// This rule's fix is marked as unsafe, as `Unpack[T]` and `*T` are considered
34+
/// different values when introspecting types at runtime. However, in most cases,
35+
/// the fix should be safe to apply.
36+
///
3337
/// [PEP 646]: https://peps.python.org/pep-0646/
3438
#[violation]
3539
pub struct NonPEP646Unpack;
@@ -57,47 +61,88 @@ pub(crate) fn use_pep646_unpack(checker: &mut Checker, expr: &ExprSubscript) {
5761
return;
5862
}
5963

60-
// Ignore `kwarg` unpacking, as in:
61-
// ```python
62-
// def f(**kwargs: Unpack[Array]):
63-
// ...
64-
// ```
65-
if checker
66-
.semantic()
67-
.current_statement()
68-
.as_function_def_stmt()
69-
.and_then(|stmt| stmt.parameters.kwarg.as_ref())
70-
.and_then(|kwarg| kwarg.annotation.as_ref())
71-
.and_then(|annotation| annotation.as_subscript_expr())
72-
.is_some_and(|subscript| subscript == expr)
73-
{
74-
return;
75-
}
76-
7764
let ExprSubscript {
7865
range,
7966
value,
8067
slice,
8168
..
8269
} = expr;
8370

71+
// Skip semantically invalid subscript calls (e.g. `Unpack[str | num]`).
72+
if !(slice.is_name_expr() || slice.is_subscript_expr() || slice.is_attribute_expr()) {
73+
return;
74+
}
75+
8476
if !checker.semantic().match_typing_expr(value, "Unpack") {
8577
return;
8678
}
8779

88-
// Skip semantically invalid subscript calls (e.g. `Unpack[str | num]`).
89-
if !(slice.is_name_expr() || slice.is_subscript_expr() || slice.is_attribute_expr()) {
80+
// Determine whether we're in a valid context for a star expression.
81+
//
82+
// Star expressions are only allowed in two places:
83+
// - Subscript indexes (e.g., `Generic[DType, *Shape]`).
84+
// - Variadic positional arguments (e.g., `def f(*args: *int)`).
85+
//
86+
// See: <https://peps.python.org/pep-0646/#grammar-changes>
87+
if !in_subscript_index(expr, checker.semantic()) && !in_vararg(expr, checker.semantic()) {
9088
return;
9189
}
9290

9391
let mut diagnostic = Diagnostic::new(NonPEP646Unpack, *range);
94-
95-
let inner = checker.locator().slice(slice.as_ref());
96-
97-
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
98-
format!("*{inner}"),
92+
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
93+
format!("*{}", checker.locator().slice(slice.as_ref())),
9994
*range,
10095
)));
101-
10296
checker.diagnostics.push(diagnostic);
10397
}
98+
99+
/// Determine whether the [`ExprSubscript`] is in a subscript index (e.g., `Generic[Unpack[int]]`).
100+
fn in_subscript_index(expr: &ExprSubscript, semantic: &SemanticModel) -> bool {
101+
let parent = semantic
102+
.current_expressions()
103+
.skip(1)
104+
.find_map(|expr| expr.as_subscript_expr());
105+
106+
let Some(parent) = parent else {
107+
return false;
108+
};
109+
110+
// E.g., `Generic[Unpack[int]]`.
111+
if parent
112+
.slice
113+
.as_subscript_expr()
114+
.is_some_and(|slice| slice == expr)
115+
{
116+
return true;
117+
}
118+
119+
// E.g., `Generic[DType, Unpack[int]]`.
120+
if parent.slice.as_tuple_expr().map_or(false, |slice| {
121+
slice
122+
.elts
123+
.iter()
124+
.any(|elt| elt.as_subscript_expr().is_some_and(|elt| elt == expr))
125+
}) {
126+
return true;
127+
}
128+
129+
false
130+
}
131+
132+
/// Determine whether the [`ExprSubscript`] is attached to a variadic argument in a function
133+
/// definition (e.g., `def f(*args: Unpack[int])`).
134+
fn in_vararg(expr: &ExprSubscript, semantic: &SemanticModel) -> bool {
135+
let parent = semantic.current_statement().as_function_def_stmt();
136+
137+
let Some(parent) = parent else {
138+
return false;
139+
};
140+
141+
parent
142+
.parameters
143+
.vararg
144+
.as_ref()
145+
.and_then(|vararg| vararg.annotation.as_ref())
146+
.and_then(|annotation| annotation.as_subscript_expr())
147+
.map_or(false, |annotation| annotation == expr)
148+
}
Original file line numberDiff line numberDiff line change
@@ -1,83 +1,92 @@
11
---
22
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
3-
snapshot_kind: text
43
---
5-
UP044.py:5:17: UP044 [*] Use `*` for unpacking
4+
UP044.py:6:17: UP044 [*] Use `*` for unpacking
65
|
7-
3 | Shape = TypeVarTuple('Shape')
8-
4 |
9-
5 | class C(Generic[Unpack[Shape]]):
6+
6 | class C(Generic[Unpack[Shape]]):
107
| ^^^^^^^^^^^^^ UP044
11-
6 | pass
8+
7 | pass
129
|
1310
= help: Convert to `*` for unpacking
1411

1512
Safe fix
16-
2 2 |
17-
3 3 | Shape = TypeVarTuple('Shape')
13+
3 3 | Shape = TypeVarTuple("Shape")
1814
4 4 |
19-
5 |-class C(Generic[Unpack[Shape]]):
20-
5 |+class C(Generic[*Shape]):
21-
6 6 | pass
22-
7 7 |
23-
8 8 | class D(Generic[Unpack [Shape]]):
15+
5 5 |
16+
6 |-class C(Generic[Unpack[Shape]]):
17+
6 |+class C(Generic[*Shape]):
18+
7 7 | pass
19+
8 8 |
20+
9 9 |
2421

25-
UP044.py:8:17: UP044 [*] Use `*` for unpacking
26-
|
27-
6 | pass
28-
7 |
29-
8 | class D(Generic[Unpack [Shape]]):
30-
| ^^^^^^^^^^^^^^^ UP044
31-
9 | pass
32-
|
33-
= help: Convert to `*` for unpacking
22+
UP044.py:10:17: UP044 [*] Use `*` for unpacking
23+
|
24+
10 | class D(Generic[Unpack[Shape]]):
25+
| ^^^^^^^^^^^^^ UP044
26+
11 | pass
27+
|
28+
= help: Convert to `*` for unpacking
3429

3530
Safe fix
36-
5 5 | class C(Generic[Unpack[Shape]]):
37-
6 6 | pass
38-
7 7 |
39-
8 |-class D(Generic[Unpack [Shape]]):
40-
8 |+class D(Generic[*Shape]):
41-
9 9 | pass
42-
10 10 |
43-
11 11 | def f(*args: Unpack[tuple[int, ...]]): pass
31+
7 7 | pass
32+
8 8 |
33+
9 9 |
34+
10 |-class D(Generic[Unpack[Shape]]):
35+
10 |+class D(Generic[*Shape]):
36+
11 11 | pass
37+
12 12 |
38+
13 13 |
4439

45-
UP044.py:11:14: UP044 [*] Use `*` for unpacking
40+
UP044.py:14:14: UP044 [*] Use `*` for unpacking
4641
|
47-
9 | pass
48-
10 |
49-
11 | def f(*args: Unpack[tuple[int, ...]]): pass
42+
14 | def f(*args: Unpack[tuple[int, ...]]):
5043
| ^^^^^^^^^^^^^^^^^^^^^^^ UP044
51-
12 |
52-
13 | def f(*args: Unpack[other.Type]): pass
44+
15 | pass
5345
|
5446
= help: Convert to `*` for unpacking
5547

5648
Safe fix
57-
8 8 | class D(Generic[Unpack [Shape]]):
58-
9 9 | pass
59-
10 10 |
60-
11 |-def f(*args: Unpack[tuple[int, ...]]): pass
61-
11 |+def f(*args: *tuple[int, ...]): pass
49+
11 11 | pass
6250
12 12 |
63-
13 13 | def f(*args: Unpack[other.Type]): pass
64-
14 14 |
51+
13 13 |
52+
14 |-def f(*args: Unpack[tuple[int, ...]]):
53+
14 |+def f(*args: *tuple[int, ...]):
54+
15 15 | pass
55+
16 16 |
56+
17 17 |
6557

66-
UP044.py:13:14: UP044 [*] Use `*` for unpacking
58+
UP044.py:18:14: UP044 [*] Use `*` for unpacking
6759
|
68-
11 | def f(*args: Unpack[tuple[int, ...]]): pass
69-
12 |
70-
13 | def f(*args: Unpack[other.Type]): pass
60+
18 | def f(*args: Unpack[other.Type]):
7161
| ^^^^^^^^^^^^^^^^^^ UP044
62+
19 | pass
7263
|
7364
= help: Convert to `*` for unpacking
7465

7566
Safe fix
76-
10 10 |
77-
11 11 | def f(*args: Unpack[tuple[int, ...]]): pass
78-
12 12 |
79-
13 |-def f(*args: Unpack[other.Type]): pass
80-
13 |+def f(*args: *other.Type): pass
81-
14 14 |
82-
15 15 |
83-
16 16 | # Not valid unpackings but they are valid syntax
67+
15 15 | pass
68+
16 16 |
69+
17 17 |
70+
18 |-def f(*args: Unpack[other.Type]):
71+
18 |+def f(*args: *other.Type):
72+
19 19 | pass
73+
20 20 |
74+
21 21 |
75+
76+
UP044.py:22:27: UP044 [*] Use `*` for unpacking
77+
|
78+
22 | def f(*args: Generic[int, Unpack[int]]):
79+
| ^^^^^^^^^^^ UP044
80+
23 | pass
81+
|
82+
= help: Convert to `*` for unpacking
83+
84+
Safe fix
85+
19 19 | pass
86+
20 20 |
87+
21 21 |
88+
22 |-def f(*args: Generic[int, Unpack[int]]):
89+
22 |+def f(*args: Generic[int, *int]):
90+
23 23 | pass
91+
24 24 |
92+
25 25 |

0 commit comments

Comments
 (0)