-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
add async121 control-flow-in-taskgroup #282
Changes from all commits
2742ad1
459c748
ff22623
402993e
3435f63
ceadc3d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -350,6 +350,59 @@ def visit_Yield(self, node: ast.Yield): | |
visit_Lambda = visit_AsyncFunctionDef | ||
|
||
|
||
@error_class | ||
class Visitor121(Flake8AsyncVisitor): | ||
error_codes: Mapping[str, str] = { | ||
"ASYNC121": ( | ||
"{0} in a {1} block behaves counterintuitively in several" | ||
" situations. Refactor to have the {0} outside." | ||
) | ||
} | ||
|
||
def __init__(self, *args: Any, **kwargs: Any): | ||
super().__init__(*args, **kwargs) | ||
self.unsafe_stack: list[str] = [] | ||
|
||
def visit_AsyncWith(self, node: ast.AsyncWith): | ||
self.save_state(node, "unsafe_stack", copy=True) | ||
|
||
for item in node.items: | ||
if get_matching_call(item.context_expr, "open_nursery", base="trio"): | ||
self.unsafe_stack.append("nursery") | ||
elif get_matching_call( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps also detect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it? I tried to repro any of the problems with asyncio, but didn't manage to #261 (comment) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left a comment there about a potential hazard that does exist with asyncio. I agree it has fewer due to the lack of user-controllable cancel scopes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added support for |
||
item.context_expr, "create_task_group", base="anyio" | ||
) or get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): | ||
self.unsafe_stack.append("task group") | ||
|
||
def visit_While(self, node: ast.While | ast.For | ast.AsyncFor): | ||
self.save_state(node, "unsafe_stack", copy=True) | ||
self.unsafe_stack.append("loop") | ||
|
||
visit_For = visit_While | ||
Zac-HD marked this conversation as resolved.
Show resolved
Hide resolved
|
||
visit_AsyncFor = visit_While | ||
|
||
def check_loop_flow(self, node: ast.Continue | ast.Break, statement: str) -> None: | ||
# self.unsafe_stack should never be empty, but no reason not to avoid a crash | ||
# for invalid code. | ||
if self.unsafe_stack and self.unsafe_stack[-1] != "loop": | ||
self.error(node, statement, self.unsafe_stack[-1]) | ||
|
||
def visit_Continue(self, node: ast.Continue) -> None: | ||
self.check_loop_flow(node, "continue") | ||
|
||
def visit_Break(self, node: ast.Break) -> None: | ||
self.check_loop_flow(node, "break") | ||
|
||
def visit_Return(self, node: ast.Return) -> None: | ||
for unsafe_cm in "nursery", "task group": | ||
if unsafe_cm in self.unsafe_stack: | ||
self.error(node, "return", unsafe_cm) | ||
|
||
def visit_FunctionDef(self, node: ast.FunctionDef): | ||
self.save_state(node, "unsafe_stack", copy=True) | ||
self.unsafe_stack = [] | ||
|
||
|
||
@error_class_cst | ||
class Visitor300(Flake8AsyncVisitor_cst): | ||
error_codes: Mapping[str, str] = { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# ASYNCIO_NO_ERROR # checked in async121_asyncio.py | ||
# ANYIO_NO_ERROR # checked in async121_anyio.py | ||
|
||
import trio | ||
from typing import Any | ||
|
||
|
||
# To avoid mypy unreachable-statement we wrap control flow calls in if statements | ||
# they should have zero effect on the visitor logic. | ||
def condition() -> bool: | ||
return False | ||
|
||
|
||
def bar() -> Any: ... | ||
|
||
|
||
async def foo_return(): | ||
async with trio.open_nursery(): | ||
if condition(): | ||
return # ASYNC121: 12, "return", "nursery" | ||
while condition(): | ||
return # ASYNC121: 12, "return", "nursery" | ||
|
||
return # safe | ||
|
||
|
||
async def foo_return_nested(): | ||
async with trio.open_nursery(): | ||
|
||
def bar(): | ||
return # safe | ||
|
||
|
||
async def foo_while_safe(): | ||
async with trio.open_nursery(): | ||
while True: | ||
if condition(): | ||
break # safe | ||
if condition(): | ||
continue # safe | ||
continue # safe | ||
|
||
|
||
async def foo_while_unsafe(): | ||
while True: | ||
async with trio.open_nursery(): | ||
if condition(): | ||
continue # ASYNC121: 16, "continue", "nursery" | ||
if condition(): | ||
break # ASYNC121: 16, "break", "nursery" | ||
if condition(): | ||
continue # safe | ||
break # safe | ||
|
||
|
||
async def foo_for_safe(): | ||
async with trio.open_nursery(): | ||
for _ in range(5): | ||
if condition(): | ||
continue # safe | ||
if condition(): | ||
break # safe | ||
|
||
|
||
async def foo_for_unsafe(): | ||
for _ in range(5): | ||
async with trio.open_nursery(): | ||
if condition(): | ||
continue # ASYNC121: 16, "continue", "nursery" | ||
if condition(): | ||
break # ASYNC121: 16, "break", "nursery" | ||
continue # safe | ||
|
||
|
||
async def foo_async_for_safe(): | ||
async with trio.open_nursery(): | ||
async for _ in bar(): | ||
if condition(): | ||
continue # safe | ||
if condition(): | ||
break # safe | ||
|
||
|
||
async def foo_async_for_unsafe(): | ||
async for _ in bar(): | ||
async with trio.open_nursery(): | ||
if condition(): | ||
continue # ASYNC121: 16, "continue", "nursery" | ||
if condition(): | ||
break # ASYNC121: 16, "break", "nursery" | ||
continue # safe | ||
|
||
|
||
# nested nursery | ||
async def foo_nested_nursery(): | ||
async with trio.open_nursery(): | ||
if condition(): | ||
return # ASYNC121: 12, "return", "nursery" | ||
async with trio.open_nursery(): | ||
if condition(): | ||
return # ASYNC121: 16, "return", "nursery" | ||
if condition(): | ||
return # ASYNC121: 12, "return", "nursery" | ||
if condition(): | ||
return # safe |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# ASYNCIO_NO_ERROR # checked in async121_asyncio.py | ||
# TRIO_NO_ERROR # checked in async121.py | ||
# BASE_LIBRARY anyio | ||
|
||
import anyio | ||
|
||
|
||
# To avoid mypy unreachable-statement we wrap control flow calls in if statements | ||
# they should have zero effect on the visitor logic. | ||
def condition() -> bool: | ||
return False | ||
|
||
|
||
# only tests that asyncio.TaskGroup is detected, main tests in async121.py | ||
async def foo_return(): | ||
while True: | ||
async with anyio.create_task_group(): | ||
if condition(): | ||
continue # ASYNC121: 16, "continue", "task group" | ||
if condition(): | ||
break # ASYNC121: 16, "break", "task group" | ||
return # ASYNC121: 12, "return", "task group" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# ANYIO_NO_ERROR | ||
# TRIO_NO_ERROR # checked in async121.py | ||
# BASE_LIBRARY asyncio | ||
# TaskGroup was added in 3.11, we run type checking with 3.9 | ||
# mypy: disable-error-code=attr-defined | ||
|
||
import asyncio | ||
|
||
|
||
# To avoid mypy unreachable-statement we wrap control flow calls in if statements | ||
# they should have zero effect on the visitor logic. | ||
def condition() -> bool: | ||
return False | ||
|
||
|
||
# only tests that asyncio.TaskGroup is detected, main tests in async121.py | ||
async def foo_return(): | ||
while True: | ||
async with asyncio.TaskGroup(): | ||
if condition(): | ||
continue # ASYNC121: 16, "continue", "task group" | ||
if condition(): | ||
break # ASYNC121: 16, "break", "task group" | ||
return # ASYNC121: 12, "return", "task group" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm, is there an
.rst
linter which can warn us about forgetting the trailing underscore on a hyperlink?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After looking through 4 linters I did find one that checked for it 🥳
https://pypi.org/project/sphinx-lint/
it, and others, ofc found a bunch of sketchy stuff. Will open PR that adds them & fixes issues