8000 Added #6: Async functions must have checkpoints on every code path · jakkdl/flake8-async@221ee28 · GitHub
[go: up one dir, main page]

Skip to content

Commit 221ee28

Browse files
committed
Added python-trio#6: Async functions must have checkpoints on every code path
1 parent f7bc1c7 commit 221ee28

File tree

8 files changed

+211
-5
lines changed

8 files changed

+211
-5
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

44
## Future
5+
- Added TRIOXXX check: Async functions must have at least one checkpoint on every code path, unless an exception is raised
56
- Add TRIO103: `except BaseException` or `except trio.Cancelled` with a code path that doesn't re-raise
67
- Add TRIO104: "Cancelled and BaseException must be re-raised" if user tries to return or raise a different exception.
78

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ pip install flake8-trio
2828
- **TRIO104**: `Cancelled` and `BaseException` must be re-raised - when a user tries to `return` or `raise` a different exception.
2929
- **TRIO105**: Calling a trio async function without immediately `await`ing it.
3030
- **TRIO106**: trio must be imported with `import trio` for the linter to work
31+
- **TRIO300**: Async functions must have at least one checkpoint on every code path, unless an exception is raised

flake8_trio.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@
1111

1212
import ast
1313
import tokenize
14-
from typing import Any, Collection, Generator, List, Optional, Tuple, Type, Union
14+
from typing import (
15+
Any,
16+
Collection,
17+
Generator,
18+
Iterable,
19+
List,
20+
Optional,
21+
Tuple,
22+
Type,
23+
Union,
24+
)
1525

1626
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
1727
__version__ = "22.7.4"
@@ -47,6 +57,13 @@ def run(cls, tree: ast.AST) -> Generator[Error, None, None]:
4757
visitor.visit(tree)
4858
yield from visitor.problems
4959

60+
def visit_nodes(self, nodes: Iterable[ast.AST]) -> None:
61+
for node in nodes:
62+
self.visit(node)
63+
64+
def error(self, error: str, lineno: int, col: int, *args: Any, **kwargs: Any):
65+
self.problems.append(make_error(error, lineno, col, *args, **kwargs))
66+
5067

5168
class TrioScope:
5269
def __init__(self, node: ast.Call, funcname: str, packagename: str):
@@ -462,6 +479,61 @@ def visit_Call(self, node: ast.Call):
462479
self.generic_visit(node)
463480

464481

482+
class Visitor300(Flake8TrioVisitor):
483+
def __init__(self) -> None:
484+
super().__init__()
485+
self.all_await = False
486+
487+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
488+
outer = self.all_await
489+
490+
self.all_await = False
491+
self.generic_visit(node)
492+
493+
if not self.all_await:
494+
self.error(TRIO300, node.lineno, node.col_offset)
495+
496+
self.all_await = outer
497+
498+
def visit_Await(
499+
self, node: Union[ast.Await, ast.AsyncFor, ast.AsyncWith, ast.Raise]
500+
):
501+
self.generic_visit(node)
502+
self.all_await = True
503+
504+
visit_AsyncFor = visit_Await
505+
visit_AsyncWith = visit_Await
506+
visit_Raise = visit_Await
507+
508+
def visit_Try(self, node: ast.Try):
509+
self.visit_nodes(node.body)
510+
511+
# disregard await's in excepts
512+
outer = self.all_await
513+
self.visit_nodes(node.handlers)
514+
self.all_await = outer
515+
516+
self.visit_nodes(node.finalbody)
517+
518+
def visit_If(self, node: ast.If):
519+
if self.all_await:
520+
self.generic_visit(node)
521+
return
522+
self.visit_nodes(node.body)
523+
body_await = self.all_await
524+
self.all_await = False
525+
526+
self.visit_nodes(node.orelse)
527+
self.all_await = body_await and self.all_await
528+
529+
def visit_While(self, node: Union[ast.While, ast.For]):
530+
outer = self.all_await
531+
self.generic_visit(node)
532+
self.all_await = outer
533+
534+
visit_For = visit_While
535+
536+
465537
class Plugin:
466538
name = __name__
467539
version = __version__
@@ -487,3 +559,4 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
487559
TRIO104 = "TRIO104: Cancelled (and therefore BaseException) must be re-raised"
488560
TRIO105 = "TRIO105: Trio async function {} must be immediately awaited"
489561
TRIO106 = "TRIO106: trio must be imported with `import trio` for the linter to work"
562+
TRIO300 = "TRIO300: Async functions must have at least one checkpoint on every code path, unless an exception is raised"

tests/test_flake8_trio.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
TRIO104,
2121
TRIO105,
2222
TRIO106,
23+
TRIO300,
2324
Error,
2425
Plugin,
2526
make_error,
@@ -94,7 +95,7 @@ def test_trio102(self):
9495
make_error(TRIO102, 92, 8),
9596
make_error(TRIO102, 94, 8),
9697
make_error(TRIO102, 101, 12),
97-
make_error(TRIO102, 123, 12),
98+
make_error(TRIO102, 124, 12),
9899
)
99100

100101
def test_trio103_104(self):
@@ -173,6 +174,20 @@ def test_trio106(self):
173174
make_error(TRIO106, 6, 0),
174175
)
175176

177+
def test_trio300(self):
178+
self.assert_expected_errors(
179+
"trio300.py",
180+
make_error(TRIO300, 10, 0),
181+
make_error(TRIO300, 15, 0),
182+
make_error(TRIO300, 28, 0),
183+
make_error(TRIO300, 33, 0),
184+
make_error(TRIO300, 46, 0),
185+
make_error(TRIO300, 51, 0),
186+
make_error(TRIO300, 59, 0),
187+
make_error(TRIO300, 90, 0),
188+
make_error(TRIO300, 99, 0),
189+
)
190+
176191

177192
@pytest.mark.fuzz
178193
class TestFuzz(unittest.TestCase):

tests/trio100_py39.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ async def function_name():
1414
trio.move_on_after(5), # error
1515
):
1616
pass
17+
await function_name() # avoid TRIO300

tests/trio102.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
async def foo():
77
try:
8-
pass
8+
await foo() # avoid TRIO300
99
finally:
1010
with trio.move_on_after(deadline=30) as s:
1111
s.shield = True
@@ -107,11 +107,12 @@ async def foo2():
107107
yield 1
108108
finally:
109109
await foo() # safe
110+
await foo() # avoid TRIO300
110111

111112

112113
async def foo3():
113114
try:
114-
pass
115+
await foo() # avoid TRIO300
115116
finally:
116117
with trio.move_on_after(30) as s, trio.fail_after(5):
117118
s.shield = True

tests/trio300.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import trio
2+
3+
_ = ""
4+
5+
6+
async def foo():
7+
await foo()
8+
9+
10+
async def foo2(): # error
11+
...
12+
13+
14+
# If
15+
async def foo_if_1(): # error
16+
if _:
17+
await foo()
18+
19+
20+
async def foo_if_2():
21+
if _:
22+
await foo()
23+
else:
24+
await foo()
25+
26+
27+
# loops
28+
async def foo_while_1(): # error
29+
while _:
30+
await foo()
31+
32+
33+
async def foo_while_2(): # error: due to not wanting to handle continue/break semantics
34+
while _:
35+
await foo()
36+
else:
37+
await foo()
38+
39+
40+
async def foo_while_3(): # safe
41+
await foo()
42+
while _:
43+
...
44+
45+
46+
async def foo_for_1(): # error
47+
for __ in _:
48+
await foo()
49+
50+
51+
async def foo_for_2(): # error: due to not wanting to handle continue/break semantics
52+
for __ in _:
53+
await foo()
54+
else:
55+
await foo()
56+
57+
58+
# try
59+
async def foo_try_1(): # error
60+
try:
61+
...
62+
except ValueError:
63+
await foo()
64+
except:
65+
await foo()
66+
67+
68+
async def foo_try_2(): # safe
69+
try:
70+
await foo()
71+
except ValueError:
72+
await foo()
73+
except:
74+
await foo()
75+
76+
77+
async def foo_try_3(): # safe
78+
try:
79+
await foo()
80+
except ValueError:
81+
await foo()
82+
except:
83+
await foo()
84+
finally:
85+
with trio.CancelScope(deadline=30, shield=True): # avoid TRIO102
86+
await foo()
87+
88+
89+
# early return
90+
async def foo_return_1(): # error
91+
return
92+
93+
94+
async def foo_return_2(): # safe
95+
await foo()
96+
return
97+
98+
99+
async def foo_return_3(): # error
100+
if _:
101+
await foo()
102+
return
103+
104+
105+
# raise
106+
async def foo_raise_1(): # safe
107+
raise ValueError()
108+
109+
110+
async def foo_raise_2(): # safe
111+
if _:
112+
await foo()
113+
else:
114+
raise ValueError()

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ ignore_errors =
2828
commands =
2929
shed
3030
flake8 --exclude .*,tests/trio*.py
31-
pyright --pythonversion 3.10
31+
pyright --pythonversion 3.10 --warnings
3232

3333
# generate py38-test py39-test and test
3434
[testenv:{py38-, py39-,}test]

0 commit comments

Comments
 (0)
0