Skip to content

Commit 90c8571

Browse files
bxffntBre
andauthored
[PT006] Fix syntax error when unpacking nested tuples in parametrize fixes (#22441) (#22464)
## Summary Fixes a syntax error bug in the `PT006` rule where applying fixes could generate invalid Python code. **Problem**: When unpacking nested tuples in `pytest.mark.parametrize` decorators, Ruff's code generator unparses tuples without outer parentheses at level 0 (e.g., `(1, 2)` becomes `1, 2` and `((1,),)` becomes `(1,),`). This caused syntax errors like `[1, 2,]` or `[(1,),,]` when used as list elements. **Solution**: Introduced two helper functions in `parametrize.rs`: - `is_parenthesized`: Validates if a string is fully enclosed in matching parentheses - `unparse_expr_in_sequence`: Ensures non-empty tuples are always parenthesized when used as sequence elements Fixes #22441 ## Test Plan - Added comprehensive regression tests to `PT006.py` covering: - Single-element nested empty tuples: `((),)` - Single-element nested single-value tuples: `((1,),)` - Single-element nested multi-value tuples: `((1, 2),)` - Deeply nested structures: `(((1,),),)` - Mixed types: `("hello",,)`, `([1, 2],,)` - Verified edge cases with automated snapshot testing: - All 47 existing flake8-pytest-style tests continue to pass - Updated snapshots: `PT006_default.snap`, `PT006_csv.snap`, `PT006_list.snap` --------- Co-authored-by: Brent Westbrook <brentrwestbrook@gmail.com>
1 parent 917929e commit 90c8571

5 files changed

Lines changed: 335 additions & 2 deletions

File tree

crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT006.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,46 @@ def test_invalid_argvalues(param):
156156
------------------------------------------------
157157
"""
158158
...
159+
160+
161+
# Regression tests for nested tuples that could cause syntax errors when unpacked.
162+
# See: https://github.com/astral-sh/ruff/issues/22441
163+
@pytest.mark.parametrize(
164+
["param"],
165+
[
166+
((),),
167+
((1,),),
168+
],
169+
)
170+
def test_single_element_nested_empty_tuple(param):
171+
...
172+
173+
174+
@pytest.mark.parametrize(
175+
["param"],
176+
[
177+
((1, 2),),
178+
((3, 4),),
179+
],
180+
)
181+
def test_single_element_nested_multi_tuple(param):
182+
...
183+
184+
185+
@pytest.mark.parametrize(
186+
["param"],
187+
[
188+
(((1,),),),
189+
],
190+
)
191+
def test_single_element_deeply_nested_tuple(param):
192+
...
193+
194+
195+
@pytest.mark.parametrize(
196+
["param"],
197+
[
198+
(((1,)),),
199+
],
200+
)
201+
def test_single_element_grouped_tuple(param): ...

crates/ruff_linter/src/rules/flake8_pytest_style/rules/parametrize.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,8 @@ fn handle_single_name(checker: &Checker, argnames: &Expr, value: &Expr, argvalue
705705
// assert isinstance(x, int) # fails because `x` is a tuple, not an int
706706
// ```
707707
let argvalues_edits = unpack_single_element_items(checker, argvalues);
708-
let argnames_edit = Edit::range_replacement(checker.generator().expr(value), argnames.range());
708+
let argnames_edit =
709+
Edit::range_replacement(unparse_expr_in_sequence(value, checker), argnames.range());
709710
let fix = if checker.comment_ranges().intersects(argnames_edit.range())
710711
|| argvalues_edits
711712
.iter()
@@ -743,13 +744,23 @@ fn unpack_single_element_items(checker: &Checker, expr: &Expr) -> Vec<Edit> {
743744
}
744745

745746
edits.push(Edit::range_replacement(
746-
checker.generator().expr(elt),
747+
unparse_expr_in_sequence(elt, checker),
747748
value.range(),
748749
));
749750
}
750751
edits
751752
}
752753

754+
fn unparse_expr_in_sequence(expr: &Expr, checker: &Checker) -> String {
755+
let content = checker.locator().slice(expr);
756+
if let Expr::Tuple(tuple) = expr {
757+
if !tuple.is_empty() && !tuple.parenthesized {
758+
return format!("({content})");
759+
}
760+
}
761+
content.to_string()
762+
}
763+
753764
fn handle_value_rows(
754765
checker: &Checker,
755766
elts: &[Expr],

crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT006_csv.snap

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,96 @@ help: Use a string for the first argument
252252
143 | [
253253
144 | (1,),
254254
145 | (2, 3),
255+
256+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
257+
--> PT006.py:164:5
258+
|
259+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
260+
163 | @pytest.mark.parametrize(
261+
164 | ["param"],
262+
| ^^^^^^^^^
263+
165 | [
264+
166 | ((),),
265+
|
266+
help: Use a string for the first argument
267+
161 | # Regression tests for nested tuples that could cause syntax errors when unpacked.
268+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
269+
163 | @pytest.mark.parametrize(
270+
- ["param"],
271+
164 + "param",
272+
165 | [
273+
- ((),),
274+
- ((1,),),
275+
166 + (),
276+
167 + (1,),
277+
168 | ],
278+
169 | )
279+
170 | def test_single_element_nested_empty_tuple(param):
280+
281+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
282+
--> PT006.py:175:5
283+
|
284+
174 | @pytest.mark.parametrize(
285+
175 | ["param"],
286+
| ^^^^^^^^^
287+
176 | [
288+
177 | ((1, 2),),
289+
|
290+
help: Use a string for the first argument
291+
172 |
292+
173 |
293+
174 | @pytest.mark.parametrize(
294+
- ["param"],
295+
175 + "param",
296+
176 | [
297+
- ((1, 2),),
298+
- ((3, 4),),
299+
177 + (1, 2),
300+
178 + (3, 4),
301+
179 | ],
302+
180 | )
303+
181 | def test_single_element_nested_multi_tuple(param):
304+
305+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
306+
--> PT006.py:186:5
307+
|
308+
185 | @pytest.mark.parametrize(
309+
186 | ["param"],
310+
| ^^^^^^^^^
311+
187 | [
312+
188 | (((1,),),),
313+
|
314+
help: Use a string for the first argument
315+
183 |
316+
184 |
317+
185 | @pytest.mark.parametrize(
318+
- ["param"],
319+
186 + "param",
320+
187 | [
321+
- (((1,),),),
322+
188 + ((1,),),
323+
189 | ],
324+
190 | )
325+
191 | def test_single_element_deeply_nested_tuple(param):
326+
327+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
328+
--> PT006.py:196:5
329+
|
330+
195 | @pytest.mark.parametrize(
331+
196 | ["param"],
332+
| ^^^^^^^^^
333+
197 | [
334+
198 | (((1,)),),
335+
|
336+
help: Use a string for the first argument
337+
193 |
338+
194 |
339+
195 | @pytest.mark.parametrize(
340+
- ["param"],
341+
196 + "param",
342+
197 | [
343+
- (((1,)),),
344+
198 + (1,),
345+
199 | ],
346+
200 | )
347+
201 | def test_single_element_grouped_tuple(param): ...

crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT006_default.snap

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,96 @@ help: Use a string for the first argument
422422
143 | [
423423
144 | (1,),
424424
145 | (2, 3),
425+
426+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
427+
--> PT006.py:164:5
428+
|
429+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
430+
163 | @pytest.mark.parametrize(
431+
164 | ["param"],
432+
| ^^^^^^^^^
433+
165 | [
434+
166 | ((),),
435+
|
436+
help: Use a string for the first argument
437+
161 | # Regression tests for nested tuples that could cause syntax errors when unpacked.
438+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
439+
163 | @pytest.mark.parametrize(
440+
- ["param"],
441+
164 + "param",
442+
165 | [
443+
- ((),),
444+
- ((1,),),
445+
166 + (),
446+
167 + (1,),
447+
168 | ],
448+
169 | )
449+
170 | def test_single_element_nested_empty_tuple(param):
450+
451+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
452+
--> PT006.py:175:5
453+
|
454+
174 | @pytest.mark.parametrize(
455+
175 | ["param"],
456+
| ^^^^^^^^^
457+
176 | [
458+
177 | ((1, 2),),
459+
|
460+
help: Use a string for the first argument
461+
172 |
462+
173 |
463+
174 | @pytest.mark.parametrize(
464+
- ["param"],
465+
175 + "param",
466+
176 | [
467+
- ((1, 2),),
468+
- ((3, 4),),
469+
177 + (1, 2),
470+
178 + (3, 4),
471+
179 | ],
472+
180 | )
473+
181 | def test_single_element_nested_multi_tuple(param):
474+
475+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
476+
--> PT006.py:186:5
477+
|
478+
185 | @pytest.mark.parametrize(
479+
186 | ["param"],
480+
| ^^^^^^^^^
481+
187 | [
482+
188 | (((1,),),),
483+
|
484+
help: Use a string for the first argument
485+
183 |
486+
184 |
487+
185 | @pytest.mark.parametrize(
488+
- ["param"],
489+
186 + "param",
490+
187 | [
491+
- (((1,),),),
492+
188 + ((1,),),
493+
189 | ],
494+
190 | )
495+
191 | def test_single_element_deeply_nested_tuple(param):
496+
497+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
498+
--> PT006.py:196:5
499+
|
500+
195 | @pytest.mark.parametrize(
501+
196 | ["param"],
502+
| ^^^^^^^^^
503+
197 | [
504+
198 | (((1,)),),
505+
|
506+
help: Use a string for the first argument
507+
193 |
508+
194 |
509+
195 | @pytest.mark.parametrize(
510+
- ["param"],
511+
196 + "param",
512+
197 | [
513+
- (((1,)),),
514+
198 + (1,),
515+
199 | ],
516+
200 | )
517+
201 | def test_single_element_grouped_tuple(param): ...

crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT006_list.snap

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,3 +384,96 @@ help: Use a string for the first argument
384384
143 | [
385385
144 | (1,),
386386
145 | (2, 3),
387+
388+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
389+
--> PT006.py:164:5
390+
|
391+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
392+
163 | @pytest.mark.parametrize(
393+
164 | ["param"],
394+
| ^^^^^^^^^
395+
165 | [
396+
166 | ((),),
397+
|
398+
help: Use a string for the first argument
399+
161 | # Regression tests for nested tuples that could cause syntax errors when unpacked.
400+
162 | # See: https://github.com/astral-sh/ruff/issues/22441
401+
163 | @pytest.mark.parametrize(
402+
- ["param"],
403+
164 + "param",
404+
165 | [
405+
- ((),),
406+
- ((1,),),
407+
166 + (),
408+
167 + (1,),
409+
168 | ],
410+
169 | )
411+
170 | def test_single_element_nested_empty_tuple(param):
412+
413+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
414+
--> PT006.py:175:5
415+
|
416+
174 | @pytest.mark.parametrize(
417+
175 | ["param"],
418+
| ^^^^^^^^^
419+
176 | [
420+
177 | ((1, 2),),
421+
|
422+
help: Use a string for the first argument
423+
172 |
424+
173 |
425+
174 | @pytest.mark.parametrize(
426+
- ["param"],
427+
175 + "param",
428+
176 | [
429+
- ((1, 2),),
430+
- ((3, 4),),
431+
177 + (1, 2),
432+
178 + (3, 4),
433+
179 | ],
434+
180 | )
435+
181 | def test_single_element_nested_multi_tuple(param):
436+
437+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
438+
--> PT006.py:186:5
439+
|
440+
185 | @pytest.mark.parametrize(
441+
186 | ["param"],
442+
| ^^^^^^^^^
443+
187 | [
444+
188 | (((1,),),),
445+
|
446+
help: Use a string for the first argument
447+
183 |
448+
184 |
449+
185 | @pytest.mark.parametrize(
450+
- ["param"],
451+
186 + "param",
452+
187 | [
453+
- (((1,),),),
454+
188 + ((1,),),
455+
189 | ],
456+
190 | )
457+
191 | def test_single_element_deeply_nested_tuple(param):
458+
459+
PT006 [*] Wrong type passed to first argument of `pytest.mark.parametrize`; expected `str`
460+
--> PT006.py:196:5
461+
|
462+
195 | @pytest.mark.parametrize(
463+
196 | ["param"],
464+
| ^^^^^^^^^
465+
197 | [
466+
198 | (((1,)),),
467+
|
468+
help: Use a string for the first argument
469+
193 |
470+
194 |
471+
195 | @pytest.mark.parametrize(
472+
- ["param"],
473+
196 + "param",
474+
197 | [
475+
- (((1,)),),
476+
198 + (1,),
477+
199 | ],
478+
200 | )
479+
201 | def test_single_element_grouped_tuple(param): ...

0 commit comments

Comments
 (0)