diff --git a/CHANGELOG.md b/CHANGELOG.md index afd64fc..3189b5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Future - Added TRIO109: Async definitions should not have a `timeout` parameter. Use `trio.[fail/move_on]_[at/after]` +- Added TRIO110: `while : await trio.sleep()` should be replaced by a `trio.Event`. ## 22.7.6 - Extend TRIO102 to also check inside `except BaseException` and `except trio.Cancelled` diff --git a/README.md b/README.md index c978f84..517e5dd 100644 --- a/README.md +++ b/README.md @@ -32,3 +32,4 @@ pip install flake8-trio - **TRIO108**: Early return from async function must have at least one checkpoint on every code path before it, unless an exception is raised. Checkpoints are `await`, `async with` `async for`. - **TRIO109**: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead +- **TRIO110**: `while : await trio.sleep()` should be replaced by a `trio.Event`. diff --git a/flake8_trio.py b/flake8_trio.py index 9717f25..1392496 100644 --- a/flake8_trio.py +++ b/flake8_trio.py @@ -154,6 +154,7 @@ def visit_AsyncWith(self, node: ast.AsyncWith): def visit_FunctionDef(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]): outer = self.get_state() self._yield_is_error = False + self._inside_loop = False # check for @ and @. if has_decorator(node.decorator_list, *context_manager_names): @@ -198,6 +199,19 @@ def check_109(self, args: ast.arguments): if arg.arg == "timeout": self.error(TRIO109, arg.lineno, arg.col_offset) + def visit_While(self, node: ast.While): + self.check_for_110(node) + self.generic_visit(node) + + def check_for_110(self, node: ast.While): + if ( + len(node.body) == 1 + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Await) + and get_trio_scope(node.body[0].value.value, "sleep", "sleep_until") + ): + self.error(TRIO110, node.lineno, node.col_offset) + def critical_except(node: ast.ExceptHandler) -> Optional[Tuple[int, int, str]]: def has_exception(node: Optional[ast.expr]) -> str: @@ -639,3 +653,4 @@ def run(self) -> Iterable[Error]: TRIO107 = "TRIO107: Async functions must have at least one checkpoint on every code path, unless an exception is raised" TRIO108 = "TRIO108: Early return from async function must have at least one checkpoint on every code path before it." TRIO109 = "TRIO109: Async function definition with a `timeout` parameter - use `trio.[fail/move_on]_[after/at]` instead" +TRIO110 = "TRIO110: `while : await trio.sleep()` should be replaced by a `trio.Event`." diff --git a/tests/trio300.py b/tests/trio109.py similarity index 100% rename from tests/trio300.py rename to tests/trio109.py diff --git a/tests/trio110.py b/tests/trio110.py new file mode 100644 index 0000000..84e19c4 --- /dev/null +++ b/tests/trio110.py @@ -0,0 +1,70 @@ +import trio +import trio as noerror + + +async def foo(): + # only trigger on while loop with body being exactly one sleep[_until] statement + while ...: # error: 4 + await trio.sleep() + + while ...: # error: 4 + await trio.sleep_until() + + # nested + + while ...: # safe + while ...: # error: 8 + await trio.sleep() + await trio.sleep() + + while ...: # safe + while ...: # error: 8 + await trio.sleep() + + ### the rest are all safe + + # don't trigger on bodies with more than one statement + while ...: + await trio.sleep() + await trio.sleep() + + while ...: # safe + ... + await trio.sleep() + + while ...: + await trio.sleep() + await trio.sleep_until() + + # check library name + while ...: + await noerror.sleep() + + async def sleep(): + ... + + while ...: + await sleep() + + # check function name + while ...: + await trio.sleepies() + + # don't trigger on [async] for + for _ in "": + await trio.sleep() + + async for _ in trio.blah: + await trio.sleep() + + while ...: + + async def blah(): + await trio.sleep() + + while ...: + if ...: + await trio.sleep() + + while await trio.sleep(): + ...