8000 Merge pull request #10 from jakkdl/trio105 · python-trio/flake8-async@b42a699 · GitHub
[go: up one dir, main page]

Skip to content

Commit b42a699

Browse files
authored
Merge pull request #10 from jakkdl/trio105
TRIO105: Calling trio async function without await
2 parents 1bd4153 + 3ee1eb8 commit b42a699

File tree

6 files changed

+147
-2
lines changed

6 files changed

+147
-2
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Changelog
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

4+
## Future
5+
- Added TRIO105 check for not immediately `await`ing async trio functions.
6+
47
## 22.7.3
58
- Added TRIO102 check for unsafe checkpoints inside `finally:` blocks
69

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ pip install flake8-trio
2424
- **TRIO101** `yield` inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling.
2525
- **TRIO102** it's unsafe to await inside `finally:` unless you use a shielded
2626
cancel scope with a timeout"
27+
- **TRIO105** Calling a trio async function without immediately `await`ing it.

flake8_trio.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,52 @@ def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]) -> None:
255255
)
256256

257257

258+
trio_async_functions = (
259+
"aclose_forcefully",
260+
"open_file",
261+
"open_ssl_over_tcp_listeners",
262+
"open_ssl_over_tcp_stream",
263+
"open_tcp_listeners",
264+
"open_tcp_stream",
265+
"open_unix_socket",
266+
"run_process",
267+
"serve_listeners",
268+
"serve_ssl_over_tcp", 10000
269+
"serve_tcp",
270+
"sleep",
271+
"sleep_forever",
272+
"sleep_until",
273+
)
274+
275+
276+
class Visitor105(ast.NodeVisitor):
277+
def __init__(self) -> None:
278+
super().__init__()
279+
self.problems: List[Error] = []
280+
self.node_stack: List[ast.AST] = []
281+
282+
def visit(self, node: ast.AST):
283+
self.node_stack.append(node)
284+
super().visit(node)
285+
self.node_stack.pop()
286+
287+
def visit_Call(self, node: ast.Call):
288+
if (
289+
isinstance(node.func, ast.Attribute)
290+
and isinstance(node.func.value, ast.Name)
291+
and node.func.value.id == "trio"
292+
and node.func.attr in trio_async_functions
293+
and (
294+
len(self.node_stack) < 2
295+
or not isinstance(self.node_stack[-2], ast.Await)
296+
)
297+
):
298+
self.problems.append(
299+
make_error(TRIO105, node.lineno, node.col_offset, node.func.attr)
300+
)
301+
self.generic_visit(node)
302+
303+
258304
class Plugin:
259305
name = __name__
260306
version = __version__
@@ -269,7 +315,7 @@ def from_filename(cls, filename: str) -> "Plugin":
269315
return cls(ast.parse(source))
270316

271317
def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
272-
for v in (Visitor, Visitor102):
318+
for v in (Visitor, Visitor102, Visitor105):
273319
visitor = v()
274320
visitor.visit(self._tree)
275321
yield from visitor.problems
@@ -278,3 +324,4 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
278324
TRIO100 = "TRIO100: {} context contains no checkpoints, add `await trio.sleep(0)`"
279325
TRIO101 = "TRIO101: yield inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling"
280326
TRIO102 = "TRIO102: it's unsafe to await inside `finally:` unless you use a shielded cancel scope with a timeout"
327+
TRIO105 = "TRIO105: Trio async function {} must be immediately awaited"

tests/test_flake8_trio.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
11
import ast
2+
import inspect
23
import os
34
import site
45
import sys
56
import unittest
67
from pathlib import Path
78

89
import pytest
10+
import trio # type: ignore
911
from hypothesis import HealthCheck, given, settings
1012
from hypothesmith import from_grammar, from_node
1113

12-
from flake8_trio import TRIO100, TRIO101, TRIO102, Error, Plugin, Visitor, make_error
14+
from flake8_trio import (
15+
TRIO100,
16+
TRIO101,
17+
TRIO102,
18+
TRIO105,
19+
Error,
20+
Plugin,
21+
Visitor,
22+
make_error,
23+
trio_async_functions,
24+
)
1325

1426

1527
class Flake8TrioTestCase(unittest.TestCase):
@@ -71,6 +83,36 @@ def test_trio102(self):
7183
make_error(TRIO102, 123, 12),
7284
)
7385

86+
def test_trio105(self):
87+
self.assert_expected_errors(
88+
"trio105.py",
89+
make_error(TRIO105, 25, 4, "aclose_forcefully"),
90+
make_error(TRIO105, 26, 4, "open_file"),
91+
make_error(TRIO105, 27, 4, "open_ssl_over_tcp_listeners"),
92+
make_error(TRIO105, 28, 4, "open_ssl_over_tcp_stream"),
93+
make_error(TRIO105, 29, 4, "open_tcp_listeners"),
94+
make_error(TRIO105, 30, 4, "open_tcp_stream"),
95+
make_error(TRIO105, 31, 4, "open_unix_socket"),
96+
make_error(TRIO105, 32, 4, "run_process"),
97+
make_error(TRIO105, 33, 4, "serve_listeners"),
98+
make_error(TRIO105, 34, 4, "serve_ssl_over_tcp"),
99+
make_error(TRIO105, 35, 4, "serve_tcp"),
100+
make_error(TRIO105, 36, 4, "sleep"),
101+
make_error(TRIO105, 37, 4, "sleep_forever"),
102+
make_error(TRIO105, 38, 4, "sleep_until"),
103+
make_error(TRIO105, 45, 15, "open_file"),
104+
make_error(TRIO105, 50, 8, "open_file"),
105+
)
106+
107+
self.assertEqual(
108+
set(trio_async_functions),
109+
{
110+
o[0]
111+
for o in inspect.getmembers(trio) # type: ignore
112+
if inspect.iscoroutinefunction(o[1])
113+
},
114+
)
115+
74116

75117
@pytest.mark.fuzz
76118
class TestFuzz(unittest.TestCase):

tests/trio105.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import trio
2+
3+
4+
async def foo():
5+
# not async
6+
trio.run()
7+
8+
# safely awaited
9+
await trio.aclose_forcefully()
10+
await trio.open_file()
11+
await trio.open_ssl_over_tcp_listeners()
12+
await trio.open_ssl_over_tcp_stream()
13+
await trio.open_tcp_listeners()
14+
await trio.open_tcp_stream()
15+
await trio.open_unix_socket()
16+
await trio.run_process()
17+
await trio.serve_listeners()
18+
await trio.serve_ssl_over_tcp()
19+
await trio.serve_tcp()
20+
await trio.sleep()
21+
await trio.sleep_forever()
22+
await trio.sleep_until()
23+
24+
# errors
25+
trio.aclose_forcefully()
26+
trio.open_file()
27+
trio.open_ssl_over_tcp_listeners()
28+
trio.open_ssl_over_tcp_stream()
29+
trio.open_tcp_listeners()
30+
trio.open_tcp_stream()
31+
trio.open_unix_socket()
32+
trio.run_process()
33+
trio.serve_listeners()
34+
trio.serve_ssl_over_tcp()
35+
trio.serve_tcp()
36+
trio.sleep()
37+
trio.sleep_forever()
38+
trio.sleep_until()
39+
40+
# safe
41+
async with await trio.open_file() as f:
42+
pass
43+
44+
# error
45+
async with trio.open_file() as f:
46+
pass
47+
48+
# safe in theory, but deemed sufficiently poor style that parsing
49+
# it isn't supported
50+
k = trio.open_file()
51+
await k

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ deps =
3535
#pytest-xdist
3636
hypothesis
3737
hypothesmith
38+
trio
3839
commands =
3940
pytest #{posargs:-n auto}
4041

0 commit comments

Comments
 (0)
0