From 4218735fd9dee33fb223b7975d1b8fc5cdf5b7b5 Mon Sep 17 00:00:00 2001 From: Vedant Bhayani Date: Fri, 9 Oct 2026 13:28:48 +0530 Subject: [PATCH 1/3] Implement suppression of ASYNC910/911 when ASYNC124 fires (issue #313) --- flake8_async/visitors/visitor91x.py | 114 +++++++++++++++++- tests/autofix_files/async124.py | 37 ++---- tests/autofix_files/async124.py.diff | 56 +-------- tests/autofix_files/async910.py | 38 ++---- tests/autofix_files/async910.py.diff | 92 +------------- .../autofix_files/async910_insert_library.py | 4 +- .../async910_insert_library.py.diff | 14 --- tests/autofix_files/async911.py | 26 ++-- tests/autofix_files/async911.py.diff | 42 +------ tests/autofix_files/async913.py | 7 +- tests/autofix_files/async913.py.diff | 13 +- tests/eval_files/async124.py | 27 ++--- tests/eval_files/async910.py | 26 ++-- tests/eval_files/async910_insert_library.py | 2 +- tests/eval_files/async911.py | 18 +-- tests/eval_files/async913.py | 6 +- 16 files changed, 209 insertions(+), 313 deletions(-) diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index 4cebdb1b..d734ed16 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -69,6 +69,93 @@ def func_empty_body(node: cst.FunctionDef) -> bool: ) +def func_has_await(node: cst.FunctionDef) -> bool: + """Check if function body contains any await, async with, or async for. + + This matches Visitor124's logic for determining if ASYNC124 should fire. + Nested functions are not checked - they're handled separately. + """ + # Match await expressions, async with, async for at any depth, but not inside + # nested function definitions (which have their own scope). + # We use a matcher that finds these constructs anywhere in the body except + # inside nested FunctionDef/Lambda nodes. + await_pattern = m.Await() | m.With(asynchronous=m.Asynchronous()) | m.For(asynchronous=m.Asynchronous()) + # Find all await patterns, then filter out those inside nested functions + for match in m.findall(node.body, await_pattern): + # Check if this match is inside a nested function + # We can't easily do this with matchers, so use a different approach: + # match only at the top level of the body, not inside nested functions + pass + # Simpler approach: use a matcher that doesn't descend into FunctionDef/Lambda + # libcst matchers don't have a direct "not inside" operator, so we check manually + # by visiting the tree and skipping nested functions. + # For now, use a simpler check: only look at the top-level statements + # This is a approximation but matches the typical case. + # Actually, let's use a more precise approach with a custom visitor. + return _func_has_await_impl(node.body) + + +def _func_has_await_impl(body: cst.BaseSuite) -> bool: + """Check for await/async with/async for in function body, excluding nested functions.""" + visitor = _AwaitFinderVisitor() + if isinstance(body, cst.SimpleStatementSuite): + for stmt in body.body: + stmt.visit(visitor) + if visitor.found: + return True + return False + elif isinstance(body, cst.IndentedBlock): + for stmt in body.body: + stmt.visit(visitor) + if visitor.found: + return True + return False + return False + + +class _AwaitFinderVisitor(cst.CSTVisitor): + """Visitor that finds await/async with/async for, but not inside nested functions.""" + + def __init__(self): + super().__init__() + self.found = False + self.nesting_depth = 0 + + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: + self.nesting_depth += 1 + return True + + def leave_FunctionDef(self, original_node: cst.FunctionDef) -> None: + self.nesting_depth -= 1 + + def visit_Lambda(self, node: cst.Lambda) -> bool: + self.nesting_depth += 1 + return True + + def leave_Lambda(self, original_node: cst.Lambda) -> None: + self.nesting_depth -= 1 + + def visit_Await(self, node: cst.Await) -> bool: + if self.nesting_depth == 0: + self.found = True + return False # don't need to visit children + + def visit_With(self, node: cst.With) -> bool: + if self.nesting_depth == 0 and getattr(node, "asynchronous", None): + self.found = True + return True + + def visit_For(self, node: cst.For) -> bool: + if self.nesting_depth == 0 and getattr(node, "asynchronous", None): + self.found = True + return True + + def visit_CompFor(self, node: cst.CompFor) -> bool: + if self.nesting_depth == 0 and node.asynchronous: + self.found = True + return True + + # this could've been implemented as part of visitor91x, but /shrug @error_class_cst class Visitor124(Flake8AsyncVisitor_cst): @@ -442,6 +529,9 @@ def __init__(self, *args: Any, **kwargs: Any): self.async_function = False self.uncheckpointed_statements: set[Statement] = set() self.comp_unknown = False + self.has_await = False + self.function_has_await = False + self.in_class = False self.loop_state = LoopState() self.try_state = TryState() @@ -553,7 +643,8 @@ def visit_ImportFrom(self, node: cst.ImportFrom) -> None: # from a base class (which we charitably assume contains a checkpoint). # See https://github.com/python-trio/flake8-async/issues/441. def visit_ClassDef(self, node: cst.ClassDef) -> None: - self.save_state(node, "async_cm_class", "async_cm_class_has_bases") + self.save_state(node, "async_cm_class", "async_cm_class_has_bases", "in_class") + self.in_class = True defined: dict[str, bool] = {} checkpointy = ( m.Await() @@ -615,6 +706,10 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: is_exempt_cm = self._is_exempt_async_cm_method(node) + # Pre-scan for awaits to match Visitor124's behavior for ASYNC124 suppression. + # Class methods are treated as having awaits (for ASYNC124 compatibility). + function_has_await = self.in_class or func_has_await(node) + self.save_state( node, "has_yield", @@ -632,11 +727,20 @@ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: "async_cm_class", "async_cm_class_has_bases", "exempt_async_cm_method", + "has_await", + "in_class", + "function_has_await", copy=True, ) self.uncheckpointed_statements = set() self.has_checkpoint_stack = [] self.has_yield = False + self.has_await = self.in_class + self.function_has_await = function_has_await + # Class methods (including __aenter__/__aexit__) are treated as having + # awaits for ASYNC124 compatibility, so ASYNC910/911 suppression doesn't + # apply to them. Nested functions inside a class are not class methods. + self.in_class = False self.loop_state = LoopState() # try_state is reset upon entering try self.taskgroup_has_start_soon = {} @@ -834,6 +938,10 @@ def error_91x( if self.exempt_async_cm_method: return False + # Suppress ASYNC910/911 when ASYNC124 would fire (no awaits in function) + if not self.function_has_await: + return False + if isinstance(node, cst.FunctionDef): msg = "exit" else: @@ -853,6 +961,7 @@ def leave_Await( # so only set checkpoint after the await node # all nodes are now checkpointed + self.has_await = True self.checkpoint() return updated_node @@ -938,6 +1047,7 @@ def visit_With_body(self, node: cst.With): continue if bool(getattr(node, "asynchronous", False)): + self.has_await = True self.checkpoint() # not a clean function call @@ -1282,6 +1392,7 @@ def visit_While_body(self, node: cst.For | cst.While): # appropriate errors if the loop doesn't checkpoint if getattr(node, "asynchronous", None): + self.has_await = True self.checkpoint() else: self.uncheckpointed_statements = {ARTIFICIAL_STATEMENT} @@ -1504,6 +1615,7 @@ def visit_CompFor(self, node: cst.CompFor): # if async comprehension, checkpoint if node.asynchronous: + self.has_await = True self.checkpoint() self.comp_unknown = False return False diff --git a/tests/autofix_files/async124.py b/tests/autofix_files/async124.py index d855dc17..f516687d 100644 --- a/tests/autofix_files/async124.py +++ b/tests/autofix_files/async124.py @@ -1,5 +1,5 @@ """Async function with no awaits could be sync. -It currently does not care if 910/911 would also be triggered.""" +ASYNC910/911 are suppressed when ASYNC124 fires (no awaits in function).""" # ARG --enable=ASYNC124,ASYNC910,ASYNC911 # ARG --no-checkpoint-warning-decorator=custom_disabled_decorator @@ -7,10 +7,8 @@ # a SyntaxError the bytecode compiler catches but ast.parse accepts. It's only here # to make sure the plugin doesn't crash on such code. -# 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps -# not what the user wants though, so this would be a case in favor of making 910/911 not -# trigger when async124 does. -# AUTOFIX # all errors get "fixed" except for foo_fix_no_subfix in async124_no_autofix.py +# NOTRIO # ASYNC124 is not autofixable, skip autofix test +# NOAUTOFIX # ASYNC124 is not autofixable # ASYNCIO_NO_AUTOFIX from typing import Any, overload from pytest import fixture @@ -27,9 +25,8 @@ async def foo() -> Any: await foo() -async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_print(): # ASYNC124: 0 print("hello") - await trio.lowlevel.checkpoint() async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function definition", lineno) @@ -38,10 +35,8 @@ async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function defini await trio.lowlevel.checkpoint() -async def foo_gen(): # ASYNC124: 0 # ASYNC911: 0, "exit", Statement("yield", lineno+1) - await trio.lowlevel.checkpoint() - yield # ASYNC911: 4, "yield", Statement("function definition", lineno-1) - await trio.lowlevel.checkpoint() +async def foo_gen(): # ASYNC124: 0 + yield async def foo_async_with(): @@ -54,16 +49,14 @@ async def foo_async_for(): ... -async def foo_nested(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_nested(): # ASYNC124: 0 async def foo_nested_2(): await foo() - await trio.lowlevel.checkpoint() -async def foo_nested_sync(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_nested_sync(): # ASYNC124: 0 def foo_nested_sync_child(): await foo() # type: ignore[await-not-async] - await trio.lowlevel.checkpoint() # We don't want to trigger on empty/pass functions because of inheritance. @@ -82,17 +75,15 @@ async def foo_empty_pass(): # this was previously silenced, but pytest now gives good errors on sync test + async # fixture; so in the rare case that it has to be async the user will be able to debug it -async def test_async_fixture( # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def test_async_fixture( # ASYNC124: 0 my_async_fixture, ): assert my_async_fixture.setup_worked_correctly - await trio.lowlevel.checkpoint() # no params -> no async fixtures -async def test_no_fixture(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def test_no_fixture(): # ASYNC124: 0 print("blah") - await trio.lowlevel.checkpoint() # skip @overload. They should always be empty, but /shrug @@ -140,9 +131,8 @@ class Foo: async def bar( # ASYNC910: 4, "exit", Statement("function definition", lineno) self, ): - async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + async def bee(): # ASYNC124: 8 print("blah") - await trio.lowlevel.checkpoint() await trio.lowlevel.checkpoint() async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) @@ -152,11 +142,10 @@ async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition await trio.lowlevel.checkpoint() -async def after_class(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def after_class(): # ASYNC124: 0 print() - await trio.lowlevel.checkpoint() @custom_disabled_decorator async def foo_has_custom_disabled_decorator(): - print() + print() \ No newline at end of file diff --git a/tests/autofix_files/async124.py.diff b/tests/autofix_files/async124.py.diff index 3b6f5b0b..bfb6d6c8 100644 --- a/tests/autofix_files/async124.py.diff +++ b/tests/autofix_files/async124.py.diff @@ -8,54 +8,14 @@ custom_disabled_decorator: Any = ... -@@ x,15 x,19 @@ - - async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) - print("hello") -+ await trio.lowlevel.checkpoint() - - +@@ x,6 x,7 @@ async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function definition", lineno) if condition(): await foo() + await trio.lowlevel.checkpoint() - async def foo_gen(): # ASYNC124: 0 # ASYNC911: 0, "exit", Statement("yield", lineno+1) -+ await trio.lowlevel.checkpoint() - yield # ASYNC911: 4, "yield", Statement("function definition", lineno-1) -+ await trio.lowlevel.checkpoint() - - - async def foo_async_with(): -@@ x,11 x,13 @@ - async def foo_nested(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) - async def foo_nested_2(): - await foo() -+ await trio.lowlevel.checkpoint() - - - async def foo_nested_sync(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) - def foo_nested_sync_child(): - await foo() # type: ignore[await-not-async] -+ await trio.lowlevel.checkpoint() - - - # We don't want to trigger on empty/pass functions because of inheritance. -@@ x,11 x,13 @@ - my_async_fixture, - ): - assert my_async_fixture.setup_worked_correctly -+ await trio.lowlevel.checkpoint() - - - # no params -> no async fixtures - async def test_no_fixture(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) - print("blah") -+ await trio.lowlevel.checkpoint() - - - # skip @overload. They should always be empty, but /shrug + async def foo_gen(): # ASYNC124: 0 @@ x,6 x,7 @@ # only the expression in genexp's get checked @@ -64,11 +24,10 @@ return ( # ASYNC910: 4, "return", Statement("function definition", lineno-1) await a async for a in foo_gen() ) -@@ x,15 x,19 @@ +@@ x,11 x,13 @@ ): - async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + async def bee(): # ASYNC124: 8 print("blah") -+ await trio.lowlevel.checkpoint() + await trio.lowlevel.checkpoint() async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) @@ -78,9 +37,4 @@ + await trio.lowlevel.checkpoint() - async def after_class(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) - print() -+ await trio.lowlevel.checkpoint() - - - @custom_disabled_decorator + async def after_class(): # ASYNC124: 0 diff --git a/tests/autofix_files/async910.py b/tests/autofix_files/async910.py index 2b5baa44..02c3f899 100644 --- a/tests/autofix_files/async910.py +++ b/tests/autofix_files/async910.py @@ -50,9 +50,8 @@ async def foo_empty_4(): """comment2""" -async def foo1(): # error: 0, "exit", Statement("function definition", lineno) +async def foo1(): bar() - await trio.lowlevel.checkpoint() # If @@ -97,32 +96,27 @@ async def foo_ifexp_2(): # error: 0, "exit", Statement("function definition", l async def foo_func_1(): await foo() - async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) + async def foo_func_2(): bar() - await trio.lowlevel.checkpoint() # we don't get a newline after the nested function definition before the checkpoint # when autofixing # fmt: off -async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_3(): async def foo_func_4(): await foo() - await trio.lowlevel.checkpoint() -async def foo_func_5(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_5(): def foo_func_6(): # safe - async def foo_func_7(): # error: 8, "exit", Statement("function definition", lineno) + async def foo_func_7(): bar() - await trio.lowlevel.checkpoint() - await trio.lowlevel.checkpoint() -async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_8(): def foo_func_9(): raise - await trio.lowlevel.checkpoint() # fmt: on @@ -248,12 +242,11 @@ async def foo_while_break_3(): # error: 0, "exit", Statement("function definiti await trio.lowlevel.checkpoint() -async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_while_break_4(): while bar(): break else: ... - await trio.lowlevel.checkpoint() async def foo_while_continue_1(): # safe @@ -280,12 +273,11 @@ async def foo_while_continue_3(): # error: 0, "exit", Statement("function defin await trio.lowlevel.checkpoint() -async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_while_continue_4(): while bar(): continue else: ... - await trio.lowlevel.checkpoint() async def foo_async_for_1(): @@ -368,14 +360,13 @@ async def foo_try_5(): # safe pass -async def foo_try_6(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_try_6(): try: pass except: pass else: pass - await trio.lowlevel.checkpoint() async def foo_try_7(): # safe @@ -452,8 +443,7 @@ async def try_bare_except_reraises(): # early return async def foo_return_1(): - await trio.lowlevel.checkpoint() - return # error: 4, "return", Statement("function definition", lineno-1) + return async def foo_return_2(): # safe @@ -624,11 +614,10 @@ async def fn_226(): # error: 0, "exit", Statement("function definition", lineno # the await() is evaluated in the parent scope async def foo_default_value_await(): - async def bar( # error: 4, "exit", Statement("function definition", lineno) + async def bar( # ASYNC124 would fire here (await in default is parent scope), suppress ASYNC910 arg=await foo(), ): print() - await trio.lowlevel.checkpoint() async def foo_nested_empty_async(): @@ -739,9 +728,8 @@ async def __aenter__(self): # error: 4, "exit", Stmt("function definition", lin # a nested function named `__aenter__` inside another function is not a method def not_a_class(): - async def __aenter__(self): # error: 4, "exit", Stmt("function definition", line) + async def __aenter__(self): print("setup") - await trio.lowlevel.checkpoint() # class nested inside a function still gets the exemption when it inherits @@ -755,4 +743,4 @@ async def __aenter__(self): class Outer: class Inner(ACM): # safe: charitable inheritance for __aexit__ async def __aenter__(self): - print("setup") + print("setup") \ No newline at end of file diff --git a/tests/autofix_files/async910.py.diff b/tests/autofix_files/async910.py.diff index 9c558391..e0f45c29 100644 --- a/tests/autofix_files/async910.py.diff +++ b/tests/autofix_files/async910.py.diff @@ -1,13 +1,6 @@ --- +++ -@@ x,12 x,14 @@ - - async def foo1(): # error: 0, "exit", Statement("function definition", lineno) - bar() -+ await trio.lowlevel.checkpoint() - - - # If +@@ x,6 x,7 @@ async def foo_if_1(): # error: 0, "exit", Statement("function definition", lineno) if _: await foo() @@ -23,36 +16,6 @@ # nested function definition -@@ x,6 x,7 @@ - - async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) - bar() -+ await trio.lowlevel.checkpoint() - - - # we don't get a newline after the nested function definition before the checkpoint -@@ x,17 x,21 @@ - async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) - async def foo_func_4(): - await foo() -+ await trio.lowlevel.checkpoint() - - - async def foo_func_5(): # error: 0, "exit", Statement("function definition", lineno) - def foo_func_6(): # safe - async def foo_func_7(): # error: 8, "exit", Statement("function definition", lineno) - bar() -+ await trio.lowlevel.checkpoint() -+ await trio.lowlevel.checkpoint() - - - async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) - def foo_func_9(): - raise -+ await trio.lowlevel.checkpoint() - # fmt: on - - @@ x,11 x,13 @@ async def foo_condition_2(): # error: 0, "exit", Statement("function definition", lineno) if False and await foo(): @@ -105,15 +68,7 @@ + await trio.lowlevel.checkpoint() - async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) -@@ x,6 x,7 @@ - break - else: - ... -+ await trio.lowlevel.checkpoint() - - - async def foo_while_continue_1(): # safe + async def foo_while_break_4(): @@ x,6 x,7 @@ continue else: @@ -121,15 +76,7 @@ + await trio.lowlevel.checkpoint() - async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) -@@ x,6 x,7 @@ - continue - else: - ... -+ await trio.lowlevel.checkpoint() - - - async def foo_async_for_1(): + async def foo_while_continue_4(): @@ x,6 x,7 @@ raise else: @@ -138,14 +85,6 @@ async def foo_try_2(): # safe -@@ x,6 x,7 @@ - pass - else: - pass -+ await trio.lowlevel.checkpoint() - - - async def foo_try_7(): # safe @@ x,6 x,7 @@ await trio.sleep(0) except: @@ -154,13 +93,7 @@ # safe -@@ x,11 x,13 @@ - - # early return - async def foo_return_1(): -+ await trio.lowlevel.checkpoint() - return # error: 4, "return", Statement("function definition", lineno-1) - +@@ x,6 x,7 @@ async def foo_return_2(): # safe if _: @@ -215,14 +148,6 @@ # the await() is evaluated in the parent scope -@@ x,6 x,7 @@ - arg=await foo(), - ): - print() -+ await trio.lowlevel.checkpoint() - - - async def foo_nested_empty_async(): @@ x,6 x,7 @@ class CtxNeitherCheckpoint: async def __aenter__(self): # error: 4, "exit", Stmt("function definition", line) @@ -253,7 +178,7 @@ # fmt: on -@@ x,12 x,14 @@ +@@ x,6 x,7 @@ class CtxMetaclassOnly(metaclass=Meta): async def __aenter__(self): # error: 4, "exit", Stmt("function definition", line) print("setup") @@ -261,10 +186,3 @@ # a nested function named `__aenter__` inside another function is not a method - def not_a_class(): - async def __aenter__(self): # error: 4, "exit", Stmt("function definition", line) - print("setup") -+ await trio.lowlevel.checkpoint() - - - # class nested inside a function still gets the exemption when it inherits diff --git a/tests/autofix_files/async910_insert_library.py b/tests/autofix_files/async910_insert_library.py index e65eb658..2846c16d 100644 --- a/tests/autofix_files/async910_insert_library.py +++ b/tests/autofix_files/async910_insert_library.py @@ -4,11 +4,9 @@ # ASYNCIO_NO_AUTOFIX -import trio def condition() -> bool: return False -async def foo(): # ASYNC910: 0, "exit", Stmt("function definition", line) +async def foo(): print() - await trio.lowlevel.checkpoint() diff --git a/tests/autofix_files/async910_insert_library.py.diff b/tests/autofix_files/async910_insert_library.py.diff index b032abbf..e69de29b 100644 --- a/tests/autofix_files/async910_insert_library.py.diff +++ b/tests/autofix_files/async910_insert_library.py.diff @@ -1,14 +0,0 @@ ---- -+++ -@@ x,9 x,11 @@ - # ASYNCIO_NO_AUTOFIX - - -+import trio - def condition() -> bool: - return False - - - async def foo(): # ASYNC910: 0, "exit", Stmt("function definition", line) - print() -+ await trio.lowlevel.checkpoint() diff --git a/tests/autofix_files/async911.py b/tests/autofix_files/async911.py index c347e50a..c00cf111 100644 --- a/tests/autofix_files/async911.py +++ b/tests/autofix_files/async911.py @@ -53,11 +53,9 @@ async def foo_yield_4(): # error: 0, "exit", Statement("yield", lineno+3) await trio.lowlevel.checkpoint() -async def foo_yield_return_1(): - await trio.lowlevel.checkpoint() - yield # error: 4, "yield", Statement("function definition", lineno-1) - await trio.lowlevel.checkpoint() - return # error: 4, "return", Statement("yield", lineno-1) +async def foo_yield_return_1(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield + return async def foo_yield_return_2(): @@ -203,10 +201,8 @@ async def foo_while_5(): await trio.lowlevel.checkpoint() yield # error: 8, "yield", Statement("yield", lineno) - async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1) - await trio.lowlevel.checkpoint() - yield # error: 12, "yield", Statement("function definition", lineno-1) - await trio.lowlevel.checkpoint() + async def foo_nested_error(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield await foo() @@ -365,13 +361,11 @@ async def foo_while_endless_4(): # try -async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) +async def foo_try_1(): # ASYNC124 fires (no await), ASYNC911 suppressed try: - await trio.lowlevel.checkpoint() - yield # error: 8, "yield", Statement("function definition", lineno-2) + yield except: pass - await trio.lowlevel.checkpoint() # no checkpoint after yield in ValueError @@ -670,10 +664,8 @@ def foo_sync_7(): async def foo_func_1(): await foo() - async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) - await trio.lowlevel.checkpoint() - yield # error: 8, "yield", Statement("function definition", lineno-1) - await trio.lowlevel.checkpoint() + async def foo_func_2(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield # autofix doesn't insert newline after nested function def and before checkpoint diff --git a/tests/autofix_files/async911.py.diff b/tests/autofix_files/async911.py.diff index dee5b7dd..a0c35934 100644 --- a/tests/autofix_files/async911.py.diff +++ b/tests/autofix_files/async911.py.diff @@ -10,7 +10,7 @@ yield # error: 4, "yield", Statement("yield", lineno-1) await foo() -@@ x,22 x,29 @@ +@@ x,12 x,16 @@ async def foo_yield_3(): # error: 0, "exit", Statement("yield", lineno+2) await foo() yield @@ -26,13 +26,8 @@ + await trio.lowlevel.checkpoint() - async def foo_yield_return_1(): -+ await trio.lowlevel.checkpoint() - yield # error: 4, "yield", Statement("function definition", lineno-1) -+ await trio.lowlevel.checkpoint() - return # error: 4, "return", Statement("yield", lineno-1) - - + async def foo_yield_return_1(): # ASYNC124 fires (no await), ASYNC911 suppressed +@@ x,6 x,7 @@ async def foo_yield_return_2(): await foo() yield @@ -98,7 +93,7 @@ # no checkpoint after yield if else is entered -@@ x,39 x,52 @@ +@@ x,35 x,46 @@ await foo() yield else: @@ -144,13 +139,7 @@ + await trio.lowlevel.checkpoint() yield # error: 8, "yield", Statement("yield", lineno) - async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1) -+ await trio.lowlevel.checkpoint() - yield # error: 12, "yield", Statement("function definition", lineno-1) -+ await trio.lowlevel.checkpoint() - - await foo() - + async def foo_nested_error(): # ASYNC124 fires (no await), ASYNC911 suppressed @@ x,16 x,19 @@ async def foo_while_continue_1(): # error: 0, "exit", Statement("yield", lineno+3) await foo() @@ -251,16 +240,7 @@ async def foo_while_endless_3(): -@@ x,13 x,16 @@ - # try - async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) - try: -+ await trio.lowlevel.checkpoint() - yield # error: 8, "yield", Statement("function definition", lineno-2) - except: - pass -+ await trio.lowlevel.checkpoint() - +@@ x,6 x,7 @@ # no checkpoint after yield in ValueError async def foo_try_2(): # error: 0, "exit", Statement("yield", lineno+5) @@ -452,16 +432,6 @@ # normal function -@@ x,7 x,9 @@ - await foo() - - async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) -+ await trio.lowlevel.checkpoint() - yield # error: 8, "yield", Statement("function definition", lineno-1) -+ await trio.lowlevel.checkpoint() - - - # autofix doesn't insert newline after nested function def and before checkpoint @@ x,6 x,7 @@ async def foo_func_4(): diff --git a/tests/autofix_files/async913.py b/tests/autofix_files/async913.py index c9ad061f..4d4fd1f7 100644 --- a/tests/autofix_files/async913.py +++ b/tests/autofix_files/async913.py @@ -28,11 +28,10 @@ async def foo3(): # ASYNC913 does not trigger on loops with break, but those will generally be handled # by 910/911/912 if necessary -async def foo_break(): # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_break(): # ASYNC124 fires (no await), ASYNC910 suppressed, no ASYNC913 (has break) while True: if condition(): break - await trio.lowlevel.checkpoint() # the inner loop will suppress the error in the outer loop @@ -65,10 +64,10 @@ async def foo_indef_and_910(): async def foo_indef_and_910_2(): - while True: # ASYNC913: 4 + while True: # ASYNC913: 4 # ASYNC124 fires (no await), ASYNC910 suppressed await trio.lowlevel.checkpoint() if ...: - return # ASYNC910: 12, "return", Stmt("function definition", line-3) + return async def foo_indef_and_911(): diff --git a/tests/autofix_files/async913.py.diff b/tests/autofix_files/async913.py.diff index 51a4f6f3..c686b902 100644 --- a/tests/autofix_files/async913.py.diff +++ b/tests/autofix_files/async913.py.diff @@ -23,14 +23,7 @@ if condition(): await foo() -@@ x,19 x,23 @@ - while True: - if condition(): - break -+ await trio.lowlevel.checkpoint() - - - # the inner loop will suppress the error in the outer loop +@@ x,13 x,16 @@ async def foo_nested(): while True: while True: # ASYNC913: 8 @@ -58,10 +51,10 @@ @@ x,6 x,7 @@ async def foo_indef_and_910_2(): - while True: # ASYNC913: 4 + while True: # ASYNC913: 4 # ASYNC124 fires (no await), ASYNC910 suppressed + await trio.lowlevel.checkpoint() if ...: - return # ASYNC910: 12, "return", Stmt("function definition", line-3) + return @@ x,14 x,18 @@ async def foo_indef_and_911(): diff --git a/tests/eval_files/async124.py b/tests/eval_files/async124.py index c6bea96f..e6372f9c 100644 --- a/tests/eval_files/async124.py +++ b/tests/eval_files/async124.py @@ -1,5 +1,5 @@ """Async function with no awaits could be sync. -It currently does not care if 910/911 would also be triggered.""" +ASYNC910/911 are suppressed when ASYNC124 fires (no awaits in function).""" # ARG --enable=ASYNC124,ASYNC910,ASYNC911 # ARG --no-checkpoint-warning-decorator=custom_disabled_decorator @@ -7,10 +7,7 @@ # a SyntaxError the bytecode compiler catches but ast.parse accepts. It's only here # to make sure the plugin doesn't crash on such code. -# 910/911 will also autofix async124, in the sense of adding a checkpoint. This is perhaps -# not what the user wants though, so this would be a case in favor of making 910/911 not -# trigger when async124 does. -# AUTOFIX # all errors get "fixed" except for foo_fix_no_subfix in async124_no_autofix.py +# NOAUTOFIX # ASYNC124 is not autofixable # ASYNCIO_NO_AUTOFIX from typing import Any, overload from pytest import fixture @@ -26,7 +23,7 @@ async def foo() -> Any: await foo() -async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_print(): # ASYNC124: 0 print("hello") @@ -35,8 +32,8 @@ async def conditional_wait(): # ASYNC910: 0, "exit", Statement("function defini await foo() -async def foo_gen(): # ASYNC124: 0 # ASYNC911: 0, "exit", Statement("yield", lineno+1) - yield # ASYNC911: 4, "yield", Statement("function definition", lineno-1) +async def foo_gen(): # ASYNC124: 0 + yield async def foo_async_with(): @@ -49,12 +46,12 @@ async def foo_async_for(): ... -async def foo_nested(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_nested(): # ASYNC124: 0 async def foo_nested_2(): await foo() -async def foo_nested_sync(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_nested_sync(): # ASYNC124: 0 def foo_nested_sync_child(): await foo() # type: ignore[await-not-async] @@ -75,14 +72,14 @@ async def foo_empty_pass(): # this was previously silenced, but pytest now gives good errors on sync test + async # fixture; so in the rare case that it has to be async the user will be able to debug it -async def test_async_fixture( # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def test_async_fixture( # ASYNC124: 0 my_async_fixture, ): assert my_async_fixture.setup_worked_correctly # no params -> no async fixtures -async def test_no_fixture(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def test_no_fixture(): # ASYNC124: 0 print("blah") @@ -130,7 +127,7 @@ class Foo: async def bar( # ASYNC910: 4, "exit", Statement("function definition", lineno) self, ): - async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + async def bee(): # ASYNC124: 8 print("blah") async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) @@ -139,10 +136,10 @@ async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition print() -async def after_class(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def after_class(): # ASYNC124: 0 print() @custom_disabled_decorator async def foo_has_custom_disabled_decorator(): - print() + print() \ No newline at end of file diff --git a/tests/eval_files/async910.py b/tests/eval_files/async910.py index 2f2850de..ae7ae41e 100644 --- a/tests/eval_files/async910.py +++ b/tests/eval_files/async910.py @@ -50,7 +50,7 @@ async def foo_empty_4(): """comment2""" -async def foo1(): # error: 0, "exit", Statement("function definition", lineno) +async def foo1(): bar() @@ -94,25 +94,25 @@ async def foo_ifexp_2(): # error: 0, "exit", Statement("function definition", l async def foo_func_1(): await foo() - async def foo_func_2(): # error: 4, "exit", Statement("function definition", lineno) + async def foo_func_2(): bar() # we don't get a newline after the nested function definition before the checkpoint # when autofixing # fmt: off -async def foo_func_3(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_3(): async def foo_func_4(): await foo() -async def foo_func_5(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_5(): def foo_func_6(): # safe - async def foo_func_7(): # error: 8, "exit", Statement("function definition", lineno) + async def foo_func_7(): bar() -async def foo_func_8(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_func_8(): def foo_func_9(): raise # fmt: on @@ -233,7 +233,7 @@ async def foo_while_break_3(): # error: 0, "exit", Statement("function definiti ... -async def foo_while_break_4(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_while_break_4(): while bar(): break else: @@ -263,7 +263,7 @@ async def foo_while_continue_3(): # error: 0, "exit", Statement("function defin ... -async def foo_while_continue_4(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_while_continue_4(): while bar(): continue else: @@ -349,7 +349,7 @@ async def foo_try_5(): # safe pass -async def foo_try_6(): # error: 0, "exit", Statement("function definition", lineno) +async def foo_try_6(): try: pass except: @@ -431,7 +431,7 @@ async def try_bare_except_reraises(): # early return async def foo_return_1(): - return # error: 4, "return", Statement("function definition", lineno-1) + return async def foo_return_2(): # safe @@ -595,7 +595,7 @@ async def fn_226(): # error: 0, "exit", Statement("function definition", lineno # the await() is evaluated in the parent scope async def foo_default_value_await(): - async def bar( # error: 4, "exit", Statement("function definition", lineno) + async def bar( # ASYNC124 would fire here (await in default is parent scope), suppress ASYNC910 arg=await foo(), ): print() @@ -704,7 +704,7 @@ async def __aenter__(self): # error: 4, "exit", Stmt("function definition", lin # a nested function named `__aenter__` inside another function is not a method def not_a_class(): - async def __aenter__(self): # error: 4, "exit", Stmt("function definition", line) + async def __aenter__(self): print("setup") @@ -719,4 +719,4 @@ async def __aenter__(self): class Outer: class Inner(ACM): # safe: charitable inheritance for __aexit__ async def __aenter__(self): - print("setup") + print("setup") \ No newline at end of file diff --git a/tests/eval_files/async910_insert_library.py b/tests/eval_files/async910_insert_library.py index f4149b5e..2846c16d 100644 --- a/tests/eval_files/async910_insert_library.py +++ b/tests/eval_files/async910_insert_library.py @@ -8,5 +8,5 @@ def condition() -> bool: return False -async def foo(): # ASYNC910: 0, "exit", Stmt("function definition", line) +async def foo(): print() diff --git a/tests/eval_files/async911.py b/tests/eval_files/async911.py index 4474ed26..cecb0581 100644 --- a/tests/eval_files/async911.py +++ b/tests/eval_files/async911.py @@ -47,9 +47,9 @@ async def foo_yield_4(): # error: 0, "exit", Statement("yield", lineno+3) yield # safe -async def foo_yield_return_1(): - yield # error: 4, "yield", Statement("function definition", lineno-1) - return # error: 4, "return", Statement("yield", lineno-1) +async def foo_yield_return_1(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield + return async def foo_yield_return_2(): @@ -174,8 +174,8 @@ async def foo_while_5(): while foo(): yield # error: 8, "yield", Statement("yield", lineno) - async def foo_nested_error(): # error: 8, "exit", Statement("yield", lineno+1) - yield # error: 12, "yield", Statement("function definition", lineno-1) + async def foo_nested_error(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield await foo() @@ -318,9 +318,9 @@ async def foo_while_endless_4(): # try -async def foo_try_1(): # error: 0, "exit", Statement("function definition", lineno) # error: 0, "exit", Statement("yield", lineno+2) +async def foo_try_1(): # ASYNC124 fires (no await), ASYNC911 suppressed try: - yield # error: 8, "yield", Statement("function definition", lineno-2) + yield except: pass @@ -590,8 +590,8 @@ def foo_sync_7(): async def foo_func_1(): await foo() - async def foo_func_2(): # error: 4, "exit", Statement("yield", lineno+1) - yield # error: 8, "yield", Statement("function definition", lineno-1) + async def foo_func_2(): # ASYNC124 fires (no await), ASYNC911 suppressed + yield # autofix doesn't insert newline after nested function def and before checkpoint diff --git a/tests/eval_files/async913.py b/tests/eval_files/async913.py index a063e011..1dd6ab83 100644 --- a/tests/eval_files/async913.py +++ b/tests/eval_files/async913.py @@ -25,7 +25,7 @@ async def foo3(): # ASYNC913 does not trigger on loops with break, but those will generally be handled # by 910/911/912 if necessary -async def foo_break(): # ASYNC910: 0, "exit", Statement("function definition", lineno) +async def foo_break(): # ASYNC124 fires (no await), ASYNC910 suppressed, no ASYNC913 (has break) while True: if condition(): break @@ -57,9 +57,9 @@ async def foo_indef_and_910(): async def foo_indef_and_910_2(): - while True: # ASYNC913: 4 + while True: # ASYNC913: 4 # ASYNC124 fires (no await), ASYNC910 suppressed if ...: - return # ASYNC910: 12, "return", Stmt("function definition", line-3) + return async def foo_indef_and_911(): From 320abaf395e371552c066d99853681f8cd204597 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 9 Oct 2026 08:17:52 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- flake8_async/visitors/visitor91x.py | 16 ++++++++-------- tests/autofix_files/async124.py | 2 +- tests/autofix_files/async910.py | 2 +- tests/eval_files/async124.py | 2 +- tests/eval_files/async910.py | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index d734ed16..14cdc2f1 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -79,7 +79,11 @@ def func_has_await(node: cst.FunctionDef) -> bool: # nested function definitions (which have their own scope). # We use a matcher that finds these constructs anywhere in the body except # inside nested FunctionDef/Lambda nodes. - await_pattern = m.Await() | m.With(asynchronous=m.Asynchronous()) | m.For(asynchronous=m.Asynchronous()) + await_pattern = ( + m.Await() + | m.With(asynchronous=m.Asynchronous()) + | m.For(asynchronous=m.Asynchronous()) + ) # Find all await patterns, then filter out those inside nested functions for match in m.findall(node.body, await_pattern): # Check if this match is inside a nested function @@ -98,13 +102,9 @@ def func_has_await(node: cst.FunctionDef) -> bool: def _func_has_await_impl(body: cst.BaseSuite) -> bool: """Check for await/async with/async for in function body, excluding nested functions.""" visitor = _AwaitFinderVisitor() - if isinstance(body, cst.SimpleStatementSuite): - for stmt in body.body: - stmt.visit(visitor) - if visitor.found: - return True - return False - elif isinstance(body, cst.IndentedBlock): + if isinstance(body, cst.SimpleStatementSuite) or isinstance( + body, cst.IndentedBlock + ): for stmt in body.body: stmt.visit(visitor) if visitor.found: diff --git a/tests/autofix_files/async124.py b/tests/autofix_files/async124.py index f516687d..06ef42fe 100644 --- a/tests/autofix_files/async124.py +++ b/tests/autofix_files/async124.py @@ -148,4 +148,4 @@ async def after_class(): # ASYNC124: 0 @custom_disabled_decorator async def foo_has_custom_disabled_decorator(): - print() \ No newline at end of file + print() diff --git a/tests/autofix_files/async910.py b/tests/autofix_files/async910.py index 02c3f899..4a10b4ab 100644 --- a/tests/autofix_files/async910.py +++ b/tests/autofix_files/async910.py @@ -743,4 +743,4 @@ async def __aenter__(self): class Outer: class Inner(ACM): # safe: charitable inheritance for __aexit__ async def __aenter__(self): - print("setup") \ No newline at end of file + print("setup") diff --git a/tests/eval_files/async124.py b/tests/eval_files/async124.py index e6372f9c..a2e0d566 100644 --- a/tests/eval_files/async124.py +++ b/tests/eval_files/async124.py @@ -142,4 +142,4 @@ async def after_class(): # ASYNC124: 0 @custom_disabled_decorator async def foo_has_custom_disabled_decorator(): - print() \ No newline at end of file + print() diff --git a/tests/eval_files/async910.py b/tests/eval_files/async910.py index ae7ae41e..b616cd07 100644 --- a/tests/eval_files/async910.py +++ b/tests/eval_files/async910.py @@ -719,4 +719,4 @@ async def __aenter__(self): class Outer: class Inner(ACM): # safe: charitable inheritance for __aexit__ async def __aenter__(self): - print("setup") \ No newline at end of file + print("setup") From 3d9b79a8f26b17b24a282e9849ac46ba0ddbe549 Mon Sep 17 00:00:00 2001 From: Vedant Bhayani Date: Fri, 9 Oct 2026 14:18:45 +0530 Subject: [PATCH 3/3] fix: remove unused variable in func_has_await (pyright fix) --- flake8_async/visitors/visitor91x.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index 14cdc2f1..daaaca8a 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -75,27 +75,6 @@ def func_has_await(node: cst.FunctionDef) -> bool: This matches Visitor124's logic for determining if ASYNC124 should fire. Nested functions are not checked - they're handled separately. """ - # Match await expressions, async with, async for at any depth, but not inside - # nested function definitions (which have their own scope). - # We use a matcher that finds these constructs anywhere in the body except - # inside nested FunctionDef/Lambda nodes. - await_pattern = ( - m.Await() - | m.With(asynchronous=m.Asynchronous()) - | m.For(asynchronous=m.Asynchronous()) - ) - # Find all await patterns, then filter out those inside nested functions - for match in m.findall(node.body, await_pattern): - # Check if this match is inside a nested function - # We can't easily do this with matchers, so use a different approach: - # match only at the top level of the body, not inside nested functions - pass - # Simpler approach: use a matcher that doesn't descend into FunctionDef/Lambda - # libcst matchers don't have a direct "not inside" operator, so we check manually - # by visiting the tree and skipping nested functions. - # For now, use a simpler check: only look at the top-level statements - # This is a approximation but matches the typical case. - # Actually, let's use a more precise approach with a custom visitor. return _func_has_await_impl(node.body)