Skip to content

Commit 2382fe1

Browse files
authored
[syntax-errors] Tuple unpacking in for statement iterator clause before Python 3.9 (#16558)
Summary -- This PR reuses a slightly modified version of the `check_tuple_unpacking` method added for detecting unpacking in `return` and `yield` statements to detect the same issue in the iterator clause of `for` loops. I ran into the same issue with a bare `for x in *rest: ...` example (invalid even on Python 3.13) and added it as a comment on #16520. I considered just making this an additional `StarTupleKind` variant as well, but this change was in a different version of Python, so I kept it separate. Test Plan -- New inline tests.
1 parent 27e9d1f commit 2382fe1

9 files changed

+673
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.8"}
2+
for x in *a, b: ...
3+
for x in a, *b: ...
4+
for x in *a, *b: ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.8"}
2+
for x in (*a, b): ...
3+
for x in ( a, *b): ...
4+
for x in (*a, *b): ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# parse_options: {"target-version": "3.9"}
2+
for x in *a, b: ...
3+
for x in a, *b: ...
4+
for x in *a, *b: ...

crates/ruff_python_parser/src/error.rs

+33
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,33 @@ pub enum UnsupportedSyntaxErrorKind {
616616
TypeParameterList,
617617
TypeAliasStatement,
618618
TypeParamDefault,
619+
620+
/// Represents the use of tuple unpacking in a `for` statement iterator clause before Python
621+
/// 3.9.
622+
///
623+
/// ## Examples
624+
///
625+
/// Like [`UnsupportedSyntaxErrorKind::StarTuple`] in `return` and `yield` statements, prior to
626+
/// Python 3.9, tuple unpacking in the iterator clause of a `for` statement required
627+
/// parentheses:
628+
///
629+
/// ```python
630+
/// # valid on Python 3.8 and earlier
631+
/// for i in (*a, *b): ...
632+
/// ```
633+
///
634+
/// Omitting the parentheses was invalid:
635+
///
636+
/// ```python
637+
/// for i in *a, *b: ... # SyntaxError
638+
/// ```
639+
///
640+
/// This was changed as part of the [PEG parser rewrite] included in Python 3.9 but not
641+
/// documented directly until the [Python 3.11 release].
642+
///
643+
/// [PEG parser rewrite]: https://peps.python.org/pep-0617/
644+
/// [Python 3.11 release]: https://docs.python.org/3/whatsnew/3.11.html#other-language-changes
645+
UnparenthesizedUnpackInFor,
619646
}
620647

621648
impl Display for UnsupportedSyntaxError {
@@ -642,6 +669,9 @@ impl Display for UnsupportedSyntaxError {
642669
UnsupportedSyntaxErrorKind::TypeParamDefault => {
643670
"Cannot set default type for a type parameter"
644671
}
672+
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => {
673+
"Cannot use iterable unpacking in `for` statements"
674+
}
645675
};
646676

647677
write!(
@@ -687,6 +717,9 @@ impl UnsupportedSyntaxErrorKind {
687717
UnsupportedSyntaxErrorKind::TypeParameterList => Change::Added(PythonVersion::PY312),
688718
UnsupportedSyntaxErrorKind::TypeAliasStatement => Change::Added(PythonVersion::PY312),
689719
UnsupportedSyntaxErrorKind::TypeParamDefault => Change::Added(PythonVersion::PY313),
720+
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor => {
721+
Change::Added(PythonVersion::PY39)
722+
}
690723
}
691724
}
692725

crates/ruff_python_parser/src/parser/expression.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -2130,7 +2130,10 @@ impl<'src> Parser<'src> {
21302130
// rest = (4, 5, 6)
21312131
// def g(): yield 1, 2, 3, *rest
21322132
// def h(): yield 1, (yield 2, *rest), 3
2133-
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Yield);
2133+
self.check_tuple_unpacking(
2134+
&parsed_expr,
2135+
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Yield),
2136+
);
21342137

21352138
Box::new(parsed_expr.expr)
21362139
});

crates/ruff_python_parser/src/parser/statement.rs

+32-5
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,10 @@ impl<'src> Parser<'src> {
406406
// # parse_options: {"target-version": "3.7"}
407407
// rest = (4, 5, 6)
408408
// def f(): return 1, 2, 3, *rest
409-
self.check_tuple_unpacking(&parsed_expr, StarTupleKind::Return);
409+
self.check_tuple_unpacking(
410+
&parsed_expr,
411+
UnsupportedSyntaxErrorKind::StarTuple(StarTupleKind::Return),
412+
);
410413

411414
Box::new(parsed_expr.expr)
412415
});
@@ -420,10 +423,12 @@ impl<'src> Parser<'src> {
420423
/// Report [`UnsupportedSyntaxError`]s for each starred element in `expr` if it is an
421424
/// unparenthesized tuple.
422425
///
423-
/// This method can be used to check for tuple unpacking in return and yield statements, which
424-
/// are only allowed in Python 3.8 and later: <https://github.com/python/cpython/issues/76298>.
425-
pub(crate) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: StarTupleKind) {
426-
let kind = UnsupportedSyntaxErrorKind::StarTuple(kind);
426+
/// This method can be used to check for tuple unpacking in `return`, `yield`, and `for`
427+
/// statements, which are only allowed after [Python 3.8] and [Python 3.9], respectively.
428+
///
429+
/// [Python 3.8]: https://github.com/python/cpython/issues/76298
430+
/// [Python 3.9]: https://github.com/python/cpython/issues/90881
431+
pub(super) fn check_tuple_unpacking(&mut self, expr: &Expr, kind: UnsupportedSyntaxErrorKind) {
427432
if kind.is_supported(self.options.target_version) {
428433
return;
429434
}
@@ -1732,6 +1737,28 @@ impl<'src> Parser<'src> {
17321737
// for target in x := 1: ...
17331738
let iter = self.parse_expression_list(ExpressionContext::starred_bitwise_or());
17341739

1740+
// test_ok for_iter_unpack_py39
1741+
// # parse_options: {"target-version": "3.9"}
1742+
// for x in *a, b: ...
1743+
// for x in a, *b: ...
1744+
// for x in *a, *b: ...
1745+
1746+
// test_ok for_iter_unpack_py38
1747+
// # parse_options: {"target-version": "3.8"}
1748+
// for x in (*a, b): ...
1749+
// for x in ( a, *b): ...
1750+
// for x in (*a, *b): ...
1751+
1752+
// test_err for_iter_unpack_py38
1753+
// # parse_options: {"target-version": "3.8"}
1754+
// for x in *a, b: ...
1755+
// for x in a, *b: ...
1756+
// for x in *a, *b: ...
1757+
self.check_tuple_unpacking(
1758+
&iter,
1759+
UnsupportedSyntaxErrorKind::UnparenthesizedUnpackInFor,
1760+
);
1761+
17351762
self.expect(TokenKind::Colon);
17361763

17371764
let body = self.parse_body(Clause::For);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
---
2+
source: crates/ruff_python_parser/tests/fixtures.rs
3+
input_file: crates/ruff_python_parser/resources/inline/err/for_iter_unpack_py38.py
4+
---
5+
## AST
6+
7+
```
8+
Module(
9+
ModModule {
10+
range: 0..106,
11+
body: [
12+
For(
13+
StmtFor {
14+
range: 43..63,
15+
is_async: false,
16+
target: Name(
17+
ExprName {
18+
range: 47..48,
19+
id: Name("x"),
20+
ctx: Store,
21+
},
22+
),
23+
iter: Tuple(
24+
ExprTuple {
25+
range: 52..58,
26+
elts: [
27+
Starred(
28+
ExprStarred {
29+
range: 52..54,
30+
value: Name(
31+
ExprName {
32+
range: 53..54,
33+
id: Name("a"),
34+
ctx: Load,
35+
},
36+
),
37+
ctx: Load,
38+
},
39+
),
40+
Name(
41+
ExprName {
42+
range: 57..58,
43+
id: Name("b"),
44+
ctx: Load,
45+
},
46+
),
47+
],
48+
ctx: Load,
49+
parenthesized: false,
50+
},
51+
),
52+
body: [
53+
Expr(
54+
StmtExpr {
55+
range: 60..63,
56+
value: EllipsisLiteral(
57+
ExprEllipsisLiteral {
58+
range: 60..63,
59+
},
60+
),
61+
},
62+
),
63+
],
64+
orelse: [],
65+
},
66+
),
67+
For(
68+
StmtFor {
69+
range: 64..84,
70+
is_async: false,
71+
target: Name(
72+
ExprName {
73+
range: 68..69,
74+
id: Name("x"),
75+
ctx: Store,
76+
},
77+
),
78+
iter: Tuple(
79+
ExprTuple {
80+
range: 74..79,
81+
elts: [
82+
Name(
83+
ExprName {
84+
range: 74..75,
85+
id: Name("a"),
86+
ctx: Load,
87+
},
88+
),
89+
Starred(
90+
ExprStarred {
91+
range: 77..79,
92+
value: Name(
93+
ExprName {
94+
range: 78..79,
95+
id: Name("b"),
96+
ctx: Load,
97+
},
98+
),
99+
ctx: Load,
100+
},
101+
),
102+
],
103+
ctx: Load,
104+
parenthesized: false,
105+
},
106+
),
107+
body: [
108+
Expr(
109+
StmtExpr {
110+
range: 81..84,
111+
value: EllipsisLiteral(
112+
ExprEllipsisLiteral {
113+
range: 81..84,
114+
},
115+
),
116+
},
117+
),
118+
],
119+
orelse: [],
120+
},
121+
),
122+
For(
123+
StmtFor {
124+
range: 85..105,
125+
is_async: false,
126+
target: Name(
127+
ExprName {
128+
range: 89..90,
129+
id: Name("x"),
130+
ctx: Store,
131+
},
132+
),
133+
iter: Tuple(
134+
ExprTuple {
135+
range: 94..100,
136+
elts: [
137+
Starred(
138+
ExprStarred {
139+
range: 94..96,
140+
value: Name(
141+
ExprName {
142+
range: 95..96,
143+
id: Name("a"),
144+
ctx: Load,
145+
},
146+
),
147+
ctx: Load,
148+
},
149+
),
150+
Starred(
151+
ExprStarred {
152+
range: 98..100,
153+
value: Name(
154+
ExprName {
155+
range: 99..100,
156+
id: Name("b"),
157+
ctx: Load,
158+
},
159+
),
160+
ctx: Load,
161+
},
162+
),
163+
],
164+
ctx: Load,
165+
parenthesized: false,
166+
},
167+
),
168+
body: [
169+
Expr(
170+
StmtExpr {
171+
range: 102..105,
172+
value: EllipsisLiteral(
173+
ExprEllipsisLiteral {
174+
range: 102..105,
175+
},
176+
),
177+
},
178+
),
179+
],
180+
orelse: [],
181+
},
182+
),
183+
],
184+
},
185+
)
186+
```
187+
## Unsupported Syntax Errors
188+
189+
|
190+
1 | # parse_options: {"target-version": "3.8"}
191+
2 | for x in *a, b: ...
192+
| ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9)
193+
3 | for x in a, *b: ...
194+
4 | for x in *a, *b: ...
195+
|
196+
197+
198+
|
199+
1 | # parse_options: {"target-version": "3.8"}
200+
2 | for x in *a, b: ...
201+
3 | for x in a, *b: ...
202+
| ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9)
203+
4 | for x in *a, *b: ...
204+
|
205+
206+
207+
|
208+
2 | for x in *a, b: ...
209+
3 | for x in a, *b: ...
210+
4 | for x in *a, *b: ...
211+
| ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9)
212+
|
213+
214+
215+
|
216+
2 | for x in *a, b: ...
217+
3 | for x in a, *b: ...
218+
4 | for x in *a, *b: ...
219+
| ^^ Syntax Error: Cannot use iterable unpacking in `for` statements on Python 3.8 (syntax was added in Python 3.9)
220+
|

0 commit comments

Comments
 (0)