diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py index 4cebdb1..daaaca8 100644 --- a/flake8_async/visitors/visitor91x.py +++ b/flake8_async/visitors/visitor91x.py @@ -69,6 +69,72 @@ 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. + """ + 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) or 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 +508,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 +622,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 +685,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 +706,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 +917,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 +940,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 +1026,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 +1371,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 +1594,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 d855dc1..06ef42f 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,9 +142,8 @@ 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 diff --git a/tests/autofix_files/async124.py.diff b/tests/autofix_files/async124.py.diff index 3b6f5b0..bfb6d6c 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 2b5baa4..4a10b4a 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 diff --git a/tests/autofix_files/async910.py.diff b/tests/autofix_files/async910.py.diff index 9c55839..e0f45c2 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 e65eb65..2846c16 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 b032abb..e69de29 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 c347e50..c00cf11 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 dee5b7d..a0c3593 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 c9ad061..4d4fd1f 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 51a4f6f..c686b90 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 c6bea96..a2e0d56 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,7 +136,7 @@ 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() diff --git a/tests/eval_files/async910.py b/tests/eval_files/async910.py index 2f2850d..b616cd0 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") diff --git a/tests/eval_files/async910_insert_library.py b/tests/eval_files/async910_insert_library.py index f4149b5..2846c16 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 4474ed2..cecb058 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 a063e01..1dd6ab8 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():