Skip to content

Commit d774a3b

Browse files
Avoid unused async when context manager includes TaskGroup (#12605)
## Summary Closes #12354.
1 parent 7e6b190 commit d774a3b

File tree

4 files changed

+46
-22
lines changed

4 files changed

+46
-22
lines changed

crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC100.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,56 +4,61 @@
44

55

66
async def func():
7-
with trio.fail_after():
7+
async with trio.fail_after():
88
...
99

1010

1111
async def func():
12-
with trio.fail_at():
12+
async with trio.fail_at():
1313
await ...
1414

1515

1616
async def func():
17-
with trio.move_on_after():
17+
async with trio.move_on_after():
1818
...
1919

2020

2121
async def func():
22-
with trio.move_at():
22+
async with trio.move_at():
2323
await ...
2424

2525

2626
async def func():
27-
with trio.move_at():
28-
async with trio.open_nursery() as nursery:
27+
async with trio.move_at():
28+
async with trio.open_nursery():
2929
...
3030

3131

3232
async def func():
33-
with anyio.move_on_after():
33+
async with anyio.move_on_after(delay=0.2):
3434
...
3535

3636

3737
async def func():
38-
with anyio.fail_after():
38+
async with anyio.fail_after():
3939
...
4040

4141

4242
async def func():
43-
with anyio.CancelScope():
43+
async with anyio.CancelScope():
4444
...
4545

4646

4747
async def func():
48-
with anyio.CancelScope():
48+
async with anyio.CancelScope():
4949
...
5050

5151

5252
async def func():
53-
with asyncio.timeout():
53+
async with asyncio.timeout(delay=0.2):
5454
...
5555

5656

5757
async def func():
58-
with asyncio.timeout_at():
58+
async with asyncio.timeout_at(when=0.2):
59+
...
60+
61+
62+
async def func():
63+
async with asyncio.timeout(delay=0.2), asyncio.TaskGroup():
5964
...

crates/ruff_linter/src/rules/flake8_async/rules/cancel_scope_no_checkpoint.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,31 @@ pub(crate) fn cancel_scope_no_checkpoint(
6969
return;
7070
}
7171

72+
// If the body contains an `await` statement, the context manager is used correctly.
7273
let mut visitor = AwaitVisitor::default();
7374
visitor.visit_body(&with_stmt.body);
7475
if visitor.seen_await {
7576
return;
7677
}
7778

79+
// If there's an `asyncio.TaskGroup()` context manager alongside the timeout, it's fine, as in:
80+
// ```python
81+
// async with asyncio.timeout(2.0), asyncio.TaskGroup():
82+
// ...
83+
// ```
84+
if with_items.iter().any(|item| {
85+
item.context_expr.as_call_expr().is_some_and(|call| {
86+
checker
87+
.semantic()
88+
.resolve_qualified_name(call.func.as_ref())
89+
.is_some_and(|qualified_name| {
90+
matches!(qualified_name.segments(), ["asyncio", "TaskGroup"])
91+
})
92+
})
93+
}) {
94+
return;
95+
}
96+
7897
if matches!(checker.settings.preview, PreviewMode::Disabled) {
7998
if matches!(method_name.module(), AsyncModule::Trio) {
8099
checker.diagnostics.push(Diagnostic::new(

crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__ASYNC100_ASYNC100.py.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_async/mod.rs
44
ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
55
|
66
6 | async def func():
7-
7 | with trio.fail_after():
7+
7 | async with trio.fail_after():
88
| _____^
99
8 | | ...
1010
| |___________^ ASYNC100
@@ -13,7 +13,7 @@ ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contai
1313
ASYNC100.py:17:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
1414
|
1515
16 | async def func():
16-
17 | with trio.move_on_after():
16+
17 | async with trio.move_on_after():
1717
| _____^
1818
18 | | ...
1919
| |___________^ ASYNC100

crates/ruff_linter/src/rules/flake8_async/snapshots/ruff_linter__rules__flake8_async__tests__preview__ASYNC100_ASYNC100.py.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ source: crates/ruff_linter/src/rules/flake8_async/mod.rs
44
ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
55
|
66
6 | async def func():
7-
7 | with trio.fail_after():
7+
7 | async with trio.fail_after():
88
| _____^
99
8 | | ...
1010
| |___________^ ASYNC100
@@ -13,7 +13,7 @@ ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contai
1313
ASYNC100.py:17:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
1414
|
1515
16 | async def func():
16-
17 | with trio.move_on_after():
16+
17 | async with trio.move_on_after():
1717
| _____^
1818
18 | | ...
1919
| |___________^ ASYNC100
@@ -22,7 +22,7 @@ ASYNC100.py:17:5: ASYNC100 A `with trio.move_on_after(...):` context does not co
2222
ASYNC100.py:33:5: ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
2323
|
2424
32 | async def func():
25-
33 | with anyio.move_on_after():
25+
33 | async with anyio.move_on_after(delay=0.2):
2626
| _____^
2727
34 | | ...
2828
| |___________^ ASYNC100
@@ -31,7 +31,7 @@ ASYNC100.py:33:5: ASYNC100 A `with anyio.move_on_after(...):` context does not c
3131
ASYNC100.py:38:5: ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
3232
|
3333
37 | async def func():
34-
38 | with anyio.fail_after():
34+
38 | async with anyio.fail_after():
3535
| _____^
3636
39 | | ...
3737
| |___________^ ASYNC100
@@ -40,7 +40,7 @@ ASYNC100.py:38:5: ASYNC100 A `with anyio.fail_after(...):` context does not cont
4040
ASYNC100.py:43:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
4141
|
4242
42 | async def func():
43-
43 | with anyio.CancelScope():
43+
43 | async with anyio.CancelScope():
4444
| _____^
4545
44 | | ...
4646
| |___________^ ASYNC100
@@ -49,7 +49,7 @@ ASYNC100.py:43:5: ASYNC100 A `with anyio.CancelScope(...):` context does not con
4949
ASYNC100.py:48:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
5050
|
5151
47 | async def func():
52-
48 | with anyio.CancelScope():
52+
48 | async with anyio.CancelScope():
5353
| _____^
5454
49 | | ...
5555
| |___________^ ASYNC100
@@ -58,7 +58,7 @@ ASYNC100.py:48:5: ASYNC100 A `with anyio.CancelScope(...):` context does not con
5858
ASYNC100.py:53:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
5959
|
6060
52 | async def func():
61-
53 | with asyncio.timeout():
61+
53 | async with asyncio.timeout(delay=0.2):
6262
| _____^
6363
54 | | ...
6464
| |___________^ ASYNC100
@@ -67,7 +67,7 @@ ASYNC100.py:53:5: ASYNC100 A `with asyncio.timeout(...):` context does not conta
6767
ASYNC100.py:58:5: ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
6868
|
6969
57 | async def func():
70-
58 | with asyncio.timeout_at():
70+
58 | async with asyncio.timeout_at(when=0.2):
7171
| _____^
7272
59 | | ...
7373
| |___________^ ASYNC100

0 commit comments

Comments
 (0)