8000 TRIO105: Calling trio async function without await by jakkdl · Pull Request #10 · python-trio/flake8-async · GitHub
[go: up one dir, main page]

Skip to content

TRIO105: Calling trio async function without await #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog
*[CalVer, YY.month.patch](https://calver.org/)*

## Future
- Added TRIO105 check for not immediately `await`ing async trio functions.

## 22.7.3
- Added TRIO102 check for unsafe checkpoints inside `finally:` blocks

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ pip install flake8-trio
- **TRIO101** `yield` inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling.
- **TRIO102** it's unsafe to await inside `finally:` unless you use a shielded
cancel scope with a timeout"
- **TRIO105** Calling a trio async function without immediately `await`ing it.
49 changes: 48 additions & 1 deletion flake8_trio.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,52 @@ def check_for_trio100(self, node: Union[ast.With, ast.AsyncWith]) -> None:
)


trio_async_functions = (
"aclose_forcefully",
"open_file",
"open_ssl_over_tcp_listeners",
"open_ssl_over_tcp_stream",
"open_tcp_listeners",
"open_tcp_stream",
"open_unix_socket",
"run_process",
"serve_listeners",
"serve_ssl_over_tcp",
"serve_tcp",
"sleep",
"sleep_forever",
"sleep_until",
)


class Visitor105(ast.NodeVisitor):
def __init__(self) -> None:
super().__init__()
self.problems: List[Error] = []
self.node_stack: List[ast.AST] = []

def visit(self, node: ast.AST):
self.node_stack.append(node)
super().visit(node)
self.node_stack.pop()

def visit_Call(self, node: ast.Call):
if (
isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.value.id == "trio"
and node.func.attr in trio_async_functions
and (
len(self.node_stack) < 2
or not isinstance(self.node_stack[-2], ast.Await)
)
):
self.problems.append(
make_error(TRIO105, node.lineno, node.col_offset, node.func.attr)
)
self.generic_visit(node)


class Plugin:
name = __name__
version = __version__
Expand All @@ -269,7 +315,7 @@ def from_filename(cls, filename: str) -> "Plugin":
return cls(ast.parse(source))

def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
for v in (Visitor, Visitor102):
for v in (Visitor, Visitor102, Visitor105):
visitor = v()
visitor.visit(self._tree)
yield from visitor.problems
Expand All @@ -278,3 +324,4 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
TRIO100 = "TRIO100: {} context contains no checkpoints, add `await trio.sleep(0)`"
TRIO101 = "TRIO101: yield inside a nursery or cancel scope is only safe when implementing a context manager - otherwise, it breaks exception handling"
TRIO102 = "TRIO102: it's unsafe to await inside `finally:` unless you use a shielded cancel scope with a timeout"
TRIO105 = "TRIO105: Trio async function {} must be immediately awaited"
44 changes: 43 additions & 1 deletion tests/test_flake8_trio.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import ast
import inspect
import os
import site
import sys
import unittest
from pathlib import Path

import pytest
import trio # type: ignore
from hypothesis import HealthCheck, given, settings
from hypothesmith import from_grammar, from_node

from flake8_trio import TRIO100, TRIO101, TRIO102, Error, Plugin, Visitor, make_error
from flake8_trio import (
TRIO100,
TRIO101,
TRIO102,
TRIO105,
Error,
Plugin,
Visitor,
make_error,
trio_async_functions,
)


class Flake8TrioTestCase(unittest.TestCase):
Expand Down Expand Up @@ -71,6 +83,36 @@ def test_trio102(self):
make_error(TRIO102, 123, 12),
)

def test_trio105(self):
self.assert_expected_errors(
"trio105.py",
make_error(TRIO105, 25, 4, "aclose_forcefully"),
make_error(TRIO105, 26, 4, "open_file"),
make_error(TRIO105, 27, 4, "open_ssl_over_tcp_listeners"),
make_error(TRIO105, 28, 4, "open_ssl_over_tcp_stream"),
make_error(TRIO105, 29, 4, "open_tcp_listeners"),
make_error(TRIO105, 30, 4, "open_tcp_stream"),
make_error(TRIO105, 31, 4, "open_unix_socket"),
make_error(TRIO105, 32, 4, "run_process"),
make_error(TRIO105, 33, 4, "serve_listeners"),
make_error(TRIO105, 34, 4, "serve_ssl_over_tcp"),
make_error(TRIO105, 35, 4, "serve_tcp"),
make_error(TRIO105, 36, 4, "sleep"),
make_error(TRIO105, 37, 4, "sleep_forever"),
make_error(TRIO105, 38, 4, "sleep_until"),
make_error(TRIO105, 45, 15, "open_file"),
make_error(TRIO105, 50, 8, "open_file"),
)

self.assertEqual(
set(trio_async_functions),
{
o[0]
for o in inspect.getmembers(trio) # type: ignore
if inspect.iscoroutinefunction(o[1])
},
)


@pytest.mark.fuzz
class TestFuzz(unittest.TestCase):
Expand Down
51 changes: 51 additions & 0 deletions tests/trio105.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import trio


async def foo():
# not async
trio.run()

# safely awaited
await trio.aclose_forcefully()
await trio.open_file()
await trio.open_ssl_over_tcp_listeners()
await trio.open_ssl_over_tcp_stream()
await trio.open_tcp_listeners()
await trio.open_tcp_stream()
await trio.open_unix_socket()
await trio.run_process()
await trio.serve_listeners()
await trio.serve_ssl_over_tcp()
await trio.serve_tcp()
await trio.sleep()
await trio.sleep_forever()
await trio.sleep_until()

# errors
trio.aclose_forcefully()
trio.open_file()
trio.open_ssl_over_tcp_listeners()
trio.open_ssl_over_tcp_stream()
trio.open_tcp_listeners()
trio.open_tcp_stream()
trio.open_unix_socket()
trio.run_process()
trio.serve_listeners()
trio.serve_ssl_over_tcp()
trio.serve_tcp()
trio.sleep()
trio.sleep_forever()
trio.sleep_until()

# safe
async with await trio.open_file() as f:
pass

# error
async with trio.open_file() as f:
pass

# safe in theory, but deemed sufficiently poor style that parsing
# it isn't supported
k = trio.open_file()
await k
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ deps =
#pytest-xdist
hypothesis
hypothesmith
trio
commands =
pytest #{posargs:-n auto}

Expand Down
0