diff --git a/docs/changelog.rst b/docs/changelog.rst index 9bd7e36..c8f5083 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog `CalVer, YY.month.patch `_ +25.2.2 +======= +- :ref:`ASYNC113 ` now only triggers on ``trio.[serve_tcp, serve_ssl_over_tcp, serve_listeners, run_process]``, instead of accepting anything as the attribute base. (e.g. :func:`anyio.run_process` is not startable). + 25.2.1 ======= - :ref:`ASYNC912 ` and :ref:`ASYNC913 ` will now trigger if there's no *cancel* points. This means that :func:`trio.open_nursery`/`anyio.create_task_group` will not silence them on their own, unless they're guaranteed to start tasks. diff --git a/docs/usage.rst b/docs/usage.rst index e6e116f..df6440f 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -33,7 +33,7 @@ adding the following to your ``.pre-commit-config.yaml``: minimum_pre_commit_version: '2.9.0' repos: - repo: https://github.com/python-trio/flake8-async - rev: 25.2.1 + rev: 25.2.2 hooks: - id: flake8-async # args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC] diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index aa715cb..8dcb1be 100644 --- a/flake8_async/__init__.py +++ b/flake8_async/__init__.py @@ -38,7 +38,7 @@ # CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1" -__version__ = "25.2.1" +__version__ = "25.2.2" # taken from https://github.com/Zac-HD/shed diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 91f47f4..92ed2b1 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -121,10 +121,10 @@ def visit_With(self, node: ast.With | ast.AsyncWith): elif get_matching_call( item.context_expr, "create_task_group", base="anyio" ): - nursery_type = "taskgroup" + nursery_type = "task group" # check for asyncio.TaskGroup elif get_matching_call(item.context_expr, "TaskGroup", base="asyncio"): - nursery_type = "taskgroup" + nursery_type = "task group" start_methods = ("create_task",) else: # incorrectly marked as not covered on py39 @@ -151,12 +151,12 @@ def visit_With(self, node: ast.With | ast.AsyncWith): # used in 113 and 114 -STARTABLE_CALLS = ( +STARTABLE_CALLS = ("serve",) +TRIO_STARTABLE_CALLS = ( "run_process", "serve_ssl_over_tcp", "serve_tcp", "serve_listeners", - "serve", ) @@ -201,7 +201,11 @@ def is_startable(n: ast.expr, *startable_list: str) -> bool: if isinstance(n, ast.Name): return n.id in startable_list if isinstance(n, ast.Attribute): - return n.attr in startable_list + return n.attr in startable_list and not ( + n.attr in TRIO_STARTABLE_CALLS + and isinstance(n.value, ast.Name) + and n.value.id != "trio" + ) if isinstance(n, ast.Call): return any(is_startable(nn, *startable_list) for nn in n.args) return False @@ -213,12 +217,16 @@ def is_nursery_call(node: ast.expr): ): return False var = ast.unparse(node.value) - return ("trio" in self.library and var.endswith("nursery")) or ( - self.variables.get(var, "") - in ( - "trio.Nursery", - "anyio.TaskGroup", - "asyncio.TaskGroup", + return ( + ("trio" in self.library and var.endswith("nursery")) + or ("anyio" in self.library and var.endswith("task_group")) + or ( + self.variables.get(var, "") + in ( + "trio.Nursery", + "anyio.TaskGroup", + "asyncio.TaskGroup", + ) ) ) @@ -229,6 +237,7 @@ def is_nursery_call(node: ast.expr): and is_startable( node.args[0], *STARTABLE_CALLS, + *TRIO_STARTABLE_CALLS, *self.options.startable_in_context_manager, ) ): @@ -254,7 +263,11 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): for n in self.walk(*node.args.args, *node.args.kwonlyargs) ) and not any( node.name == opt - for opt in (*self.options.startable_in_context_manager, *STARTABLE_CALLS) + for opt in ( + *self.options.startable_in_context_manager, + *STARTABLE_CALLS, + *TRIO_STARTABLE_CALLS, + ) ): self.error(node, node.name) diff --git a/tests/autofix_files/async100.py b/tests/autofix_files/async100.py index bfcad5f..2c081cd 100644 --- a/tests/autofix_files/async100.py +++ b/tests/autofix_files/async100.py @@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call(): async def another_weird_with_call(): async with a().b(): ... + + +# ---open_nursery / create_task_group stuff--- + + +async def nursery_no_cancel_point(): + # error: 9, "trio", "CancelScope" + async with trio.open_nursery(): + ... + + +# but it is a cancel point if the nursery contains a call to start_soon() + + +async def nursery_start_soon(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: + # error: 13, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +async def nested_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nested_nursery(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + async with trio.open_nursery() as n2: + n2.start_soon(trio.sleep, 0) + + +async def nested_function_call(): + + # error: 9, "trio", "CancelScope" + async with trio.open_nursery() as n: + + def foo(): + n.start_soon(trio.sleep, 0) + + # a false alarm in case we call foo()... but we can't check if they do + foo() + + +# insert cancel point on nursery exit, not at the start_soon call +async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + # error: 17, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +# async100 does not consider *redundant* cancel scopes +async def redundant_cancel_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + await trio.lowlevel.checkpoint() + + +# but if it did then none of these scopes should be marked redundant +# The inner checks task startup, the outer checks task exit +async def nursery_exit_blocks_with_start(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): + await n.start(trio.sleep, 0) diff --git a/tests/autofix_files/async100.py.diff b/tests/autofix_files/async100.py.diff index 268f7d8..a6095a2 100644 --- a/tests/autofix_files/async100.py.diff +++ b/tests/autofix_files/async100.py.diff @@ -96,14 +96,15 @@ await trio.sleep_forever() - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... -+ # error: 13, "trio", "CancelScope" -+ ... - +- - with trio.fail_after(1): # error: 9, "trio", "fail_after" - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - ... ++ # error: 13, "trio", "CancelScope" ++ ... ++ + # error: 9, "trio", "fail_after" + # error: 13, "trio", "CancelScope" + ... @@ -119,10 +120,11 @@ - with trio.fail_after(1): # error: 9, "trio", "fail_after" - with contextlib.suppress(Exception): - print("foo") -+ # error: 9, "trio", "fail_after" - with contextlib.suppress(Exception): +- with contextlib.suppress(Exception): - with trio.fail_after(1): # error: 13, "trio", "fail_after" - print("foo") ++ # error: 9, "trio", "fail_after" ++ with contextlib.suppress(Exception): + print("foo") + with contextlib.suppress(Exception): + # error: 13, "trio", "fail_after" @@ -130,3 +132,60 @@ with contextlib.suppress(Exception): with open("blah") as file: +@@ x,9 x,9 @@ + + + async def nursery_no_cancel_point(): +- with trio.CancelScope(): # error: 9, "trio", "CancelScope" +- async with trio.open_nursery(): +- ... ++ # error: 9, "trio", "CancelScope" ++ async with trio.open_nursery(): ++ ... + + + # but it is a cancel point if the nursery contains a call to start_soon() +@@ x,8 x,8 @@ + + async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: +- with trio.CancelScope(): # error: 13, "trio", "CancelScope" +- n.start_soon(trio.sleep, 0) ++ # error: 13, "trio", "CancelScope" ++ n.start_soon(trio.sleep, 0) + + + async def nested_scope(): +@@ x,22 x,22 @@ + + async def nested_function_call(): + +- with trio.CancelScope(): # error: 9, "trio", "CancelScope" +- async with trio.open_nursery() as n: +- +- def foo(): +- n.start_soon(trio.sleep, 0) +- +- # a false alarm in case we call foo()... but we can't check if they do +- foo() ++ # error: 9, "trio", "CancelScope" ++ async with trio.open_nursery() as n: ++ ++ def foo(): ++ n.start_soon(trio.sleep, 0) ++ ++ # a false alarm in case we call foo()... but we can't check if they do ++ foo() + + + # insert cancel point on nursery exit, not at the start_soon call + async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: +- with trio.CancelScope(): # error: 17, "trio", "CancelScope" +- n.start_soon(trio.sleep, 0) ++ # error: 17, "trio", "CancelScope" ++ n.start_soon(trio.sleep, 0) + + + # async100 does not consider *redundant* cancel scopes diff --git a/tests/autofix_files/async100_anyio.py b/tests/autofix_files/async100_anyio.py deleted file mode 100644 index 80516cf..0000000 --- a/tests/autofix_files/async100_anyio.py +++ /dev/null @@ -1,26 +0,0 @@ -# AUTOFIX -# BASE_LIBRARY anyio -# NOTRIO # trio.create_task_group doesn't exist -# ASYNCIO_NO_ERROR -import anyio - - -async def bar() -> None: ... - - -async def anyio_cancelscope(): - # error: 9, "anyio", "CancelScope" - ... - - -# see async100_trio for more comprehensive tests -async def nursery_no_cancel_point(): - # error: 9, "anyio", "CancelScope" - async with anyio.create_task_group(): - ... - - -async def nursery_with_start_soon(): - with anyio.CancelScope(): - async with anyio.create_task_group() as tg: - tg.start_soon(bar) diff --git a/tests/autofix_files/async100_anyio.py.diff b/tests/autofix_files/async100_anyio.py.diff deleted file mode 100644 index 3e57cd9..0000000 --- a/tests/autofix_files/async100_anyio.py.diff +++ /dev/null @@ -1,23 +0,0 @@ ---- -+++ -@@ x,15 x,15 @@ - - - async def anyio_cancelscope(): -- with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" -- ... -+ # error: 9, "anyio", "CancelScope" -+ ... - - - # see async100_trio for more comprehensive tests - async def nursery_no_cancel_point(): -- with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" -- async with anyio.create_task_group(): -- ... -+ # error: 9, "anyio", "CancelScope" -+ async with anyio.create_task_group(): -+ ... - - - async def nursery_with_start_soon(): diff --git a/tests/autofix_files/async100_trio.py b/tests/autofix_files/async100_trio.py deleted file mode 100644 index ed61139..0000000 --- a/tests/autofix_files/async100_trio.py +++ /dev/null @@ -1,75 +0,0 @@ -# AUTOFIX -# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -# NOANYIO # anyio.open_nursery doesn't exist -import trio - - -async def nursery_no_cancel_point(): - # error: 9, "trio", "CancelScope" - async with trio.open_nursery(): - ... - - -# but it is a cancel point if the nursery contains a call to start_soon() - - -async def nursery_start_soon(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: - # error: 13, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -async def nested_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nested_nursery(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - async with trio.open_nursery() as n2: - n2.start_soon(trio.sleep, 0) - - -async def nested_function_call(): - - # error: 9, "trio", "CancelScope" - async with trio.open_nursery() as n: - - def foo(): - n.start_soon(trio.sleep, 0) - - # a false alarm in case we call foo()... but we can't check if they do - foo() - - -# insert cancel point on nursery exit, not at the start_soon call -async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - # error: 17, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -# async100 does not consider *redundant* cancel scopes -async def redundant_cancel_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - await trio.lowlevel.checkpoint() - - -# but if it did then none of these scopes should be marked redundant -# The inner checks task startup, the outer checks task exit -async def nursery_exit_blocks_with_start(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): - await n.start(trio.sleep, 0) diff --git a/tests/autofix_files/async100_trio.py.diff b/tests/autofix_files/async100_trio.py.diff deleted file mode 100644 index e9ff0d1..0000000 --- a/tests/autofix_files/async100_trio.py.diff +++ /dev/null @@ -1,57 +0,0 @@ ---- -+++ -@@ x,9 x,9 @@ - - - async def nursery_no_cancel_point(): -- with trio.CancelScope(): # error: 9, "trio", "CancelScope" -- async with trio.open_nursery(): -- ... -+ # error: 9, "trio", "CancelScope" -+ async with trio.open_nursery(): -+ ... - - - # but it is a cancel point if the nursery contains a call to start_soon() -@@ x,8 x,8 @@ - - async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: -- with trio.CancelScope(): # error: 13, "trio", "CancelScope" -- n.start_soon(trio.sleep, 0) -+ # error: 13, "trio", "CancelScope" -+ n.start_soon(trio.sleep, 0) - - - async def nested_scope(): -@@ x,22 x,22 @@ - - async def nested_function_call(): - -- with trio.CancelScope(): # error: 9, "trio", "CancelScope" -- async with trio.open_nursery() as n: -+ # error: 9, "trio", "CancelScope" -+ async with trio.open_nursery() as n: - -- def foo(): -- n.start_soon(trio.sleep, 0) -+ def foo(): -+ n.start_soon(trio.sleep, 0) - -- # a false alarm in case we call foo()... but we can't check if they do -- foo() -+ # a false alarm in case we call foo()... but we can't check if they do -+ foo() - - - # insert cancel point on nursery exit, not at the start_soon call - async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: -- with trio.CancelScope(): # error: 17, "trio", "CancelScope" -- n.start_soon(trio.sleep, 0) -+ # error: 17, "trio", "CancelScope" -+ n.start_soon(trio.sleep, 0) - - - # async100 does not consider *redundant* cancel scopes diff --git a/tests/autofix_files/async124.py b/tests/autofix_files/async124.py new file mode 100644 index 0000000..e514bba --- /dev/null +++ b/tests/autofix_files/async124.py @@ -0,0 +1,159 @@ +"""Async function with no awaits could be sync. +It currently does not care if 910/911 would also be triggered.""" + +# ARG --enable=ASYNC124,ASYNC910,ASYNC911 +# ARG --no-checkpoint-warning-decorator=custom_disabled_decorator + +# 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 +# ASYNCIO_NO_AUTOFIX +from typing import Any, overload +from pytest import fixture +import trio + +custom_disabled_decorator: Any = ... + + +def condition() -> bool: + return False + + +async def foo() -> Any: + await foo() + + +async def foo_print(): # ASYNC124: 0 # ASYNC910: 0, "exit", Statement("function definition", lineno) + print("hello") + await trio.lowlevel.checkpoint() + + +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(): + async with foo_gen(): + ... + + +async def foo_async_for(): + async for i in foo_gen(): + ... + + +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. +# Uses same logic as async91x. + + +async def foo_empty(): + "blah" + ... + + +async def foo_empty_pass(): + "foo" + 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) + 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 +@overload +async def foo_overload(): + print("blah") + + +async def foo_overload(): ... + + +# skip @[pytest.]fixture if they have any params, since they might depend on other +# async fixtures +@fixture +async def foo_fix(my_async_fixture): + print("blah") + + +# @fixture with no params can be converted to sync +# see async124_no_autofix.py + + +async def default_value(): + def foo(arg=await foo()): ... + + +# only the expression in genexp's get checked +async def foo_async_gen(): # ASYNC124: 0 + await trio.lowlevel.checkpoint() + return ( # ASYNC910: 4, "return", Statement("function definition", lineno-1) + await a async for a in foo_gen() + ) + + +async def foo_async_gen_await(): + return (a for a in await foo_gen()) + + +async def foo_async_for_comprehension(): + return [a async for a in foo_gen()] + + +class Foo: + # async124 ignores class methods + async def bar( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + print("blah") + await trio.lowlevel.checkpoint() + await trio.lowlevel.checkpoint() + + async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + print() + 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 foo_has_custom_disabled_decorator(): + print() diff --git a/tests/autofix_files/async124.py.diff b/tests/autofix_files/async124.py.diff new file mode 100644 index 0000000..3b6f5b0 --- /dev/null +++ b/tests/autofix_files/async124.py.diff @@ -0,0 +1,86 @@ +--- ++++ +@@ x,6 x,7 @@ + # ASYNCIO_NO_AUTOFIX + from typing import Any, overload + from pytest import fixture ++import trio + + 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() + + + 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 +@@ x,6 x,7 @@ + + # only the expression in genexp's get checked + async def foo_async_gen(): # ASYNC124: 0 ++ await trio.lowlevel.checkpoint() + return ( # ASYNC910: 4, "return", Statement("function definition", lineno-1) + await a async for a in foo_gen() + ) +@@ x,15 x,19 @@ + ): + async def bee(): # ASYNC124: 8 # ASYNC910: 8, "exit", Statement("function definition", lineno) + print("blah") ++ await trio.lowlevel.checkpoint() ++ await trio.lowlevel.checkpoint() + + async def later_in_class( # ASYNC910: 4, "exit", Statement("function definition", lineno) + self, + ): + print() ++ 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 diff --git a/tests/eval_files/async100.py b/tests/eval_files/async100.py index 2c6cf47..c51a126 100644 --- a/tests/eval_files/async100.py +++ b/tests/eval_files/async100.py @@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call(): async def another_weird_with_call(): async with a().b(): ... + + +# ---open_nursery / create_task_group stuff--- + + +async def nursery_no_cancel_point(): + with trio.CancelScope(): # error: 9, "trio", "CancelScope" + async with trio.open_nursery(): + ... + + +# but it is a cancel point if the nursery contains a call to start_soon() + + +async def nursery_start_soon(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nursery_start_soon_misnested(): + async with trio.open_nursery() as n: + with trio.CancelScope(): # error: 13, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +async def nested_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + n.start_soon(trio.sleep, 0) + + +async def nested_nursery(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + async with trio.open_nursery() as n2: + n2.start_soon(trio.sleep, 0) + + +async def nested_function_call(): + + with trio.CancelScope(): # error: 9, "trio", "CancelScope" + async with trio.open_nursery() as n: + + def foo(): + n.start_soon(trio.sleep, 0) + + # a false alarm in case we call foo()... but we can't check if they do + foo() + + +# insert cancel point on nursery exit, not at the start_soon call +async def cancel_point_on_nursery_exit(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): # error: 17, "trio", "CancelScope" + n.start_soon(trio.sleep, 0) + + +# async100 does not consider *redundant* cancel scopes +async def redundant_cancel_scope(): + with trio.CancelScope(): + with trio.CancelScope(): + await trio.lowlevel.checkpoint() + + +# but if it did then none of these scopes should be marked redundant +# The inner checks task startup, the outer checks task exit +async def nursery_exit_blocks_with_start(): + with trio.CancelScope(): + async with trio.open_nursery() as n: + with trio.CancelScope(): + await n.start(trio.sleep, 0) diff --git a/tests/eval_files/async100_anyio.py b/tests/eval_files/async100_anyio.py deleted file mode 100644 index f274969..0000000 --- a/tests/eval_files/async100_anyio.py +++ /dev/null @@ -1,26 +0,0 @@ -# AUTOFIX -# BASE_LIBRARY anyio -# NOTRIO # trio.create_task_group doesn't exist -# ASYNCIO_NO_ERROR -import anyio - - -async def bar() -> None: ... - - -async def anyio_cancelscope(): - with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" - ... - - -# see async100_trio for more comprehensive tests -async def nursery_no_cancel_point(): - with anyio.CancelScope(): # error: 9, "anyio", "CancelScope" - async with anyio.create_task_group(): - ... - - -async def nursery_with_start_soon(): - with anyio.CancelScope(): - async with anyio.create_task_group() as tg: - tg.start_soon(bar) diff --git a/tests/eval_files/async100_trio.py b/tests/eval_files/async100_trio.py deleted file mode 100644 index d722185..0000000 --- a/tests/eval_files/async100_trio.py +++ /dev/null @@ -1,75 +0,0 @@ -# AUTOFIX -# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist -# NOANYIO # anyio.open_nursery doesn't exist -import trio - - -async def nursery_no_cancel_point(): - with trio.CancelScope(): # error: 9, "trio", "CancelScope" - async with trio.open_nursery(): - ... - - -# but it is a cancel point if the nursery contains a call to start_soon() - - -async def nursery_start_soon(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nursery_start_soon_misnested(): - async with trio.open_nursery() as n: - with trio.CancelScope(): # error: 13, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -async def nested_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - n.start_soon(trio.sleep, 0) - - -async def nested_nursery(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - async with trio.open_nursery() as n2: - n2.start_soon(trio.sleep, 0) - - -async def nested_function_call(): - - with trio.CancelScope(): # error: 9, "trio", "CancelScope" - async with trio.open_nursery() as n: - - def foo(): - n.start_soon(trio.sleep, 0) - - # a false alarm in case we call foo()... but we can't check if they do - foo() - - -# insert cancel point on nursery exit, not at the start_soon call -async def cancel_point_on_nursery_exit(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): # error: 17, "trio", "CancelScope" - n.start_soon(trio.sleep, 0) - - -# async100 does not consider *redundant* cancel scopes -async def redundant_cancel_scope(): - with trio.CancelScope(): - with trio.CancelScope(): - await trio.lowlevel.checkpoint() - - -# but if it did then none of these scopes should be marked redundant -# The inner checks task startup, the outer checks task exit -async def nursery_exit_blocks_with_start(): - with trio.CancelScope(): - async with trio.open_nursery() as n: - with trio.CancelScope(): - await n.start(trio.sleep, 0) diff --git a/tests/eval_files/async101.py b/tests/eval_files/async101.py index 1623f24..f936b87 100644 --- a/tests/eval_files/async101.py +++ b/tests/eval_files/async101.py @@ -2,10 +2,6 @@ # ARG --no-checkpoint-warning-decorator=no_checkpoint_warning_decorator # ARG --transform-async-generator-decorators=transform_async_gen_decorator -# This file contains errors shared between trio and anyio, since they have some -# overlap in naming. -# See async101_xxx which has errors specific to trio/asyncio/anyio. - import contextlib import contextlib as bla @@ -149,3 +145,14 @@ def no_checkpoint_warning_deco_fun(): def transfor_async_gen_deco_fun(): with trio.CancelScope(): yield 1 # safe + + +async def foo_open_nursery(): + async with trio.open_nursery() as _: + yield 1 # error: 8 + + +@asynccontextmanager +async def foo_open_nursery_contextmanager(): + async with trio.open_nursery() as _: + yield 1 # safe diff --git a/tests/eval_files/async101_anyio.py b/tests/eval_files/async101_anyio.py deleted file mode 100644 index 9f16b8c..0000000 --- a/tests/eval_files/async101_anyio.py +++ /dev/null @@ -1,10 +0,0 @@ -# BASE_LIBRARY anyio -# TRIO_NO_ERROR -# ASYNCIO_NO_ERROR - -import anyio - - -async def foo(): - async with anyio.create_task_group(): - yield 1 # error: 8 diff --git a/tests/eval_files/async101_trio.py b/tests/eval_files/async101_trio.py deleted file mode 100644 index 3ceda36..0000000 --- a/tests/eval_files/async101_trio.py +++ /dev/null @@ -1,17 +0,0 @@ -# ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR - -from contextlib import asynccontextmanager - -import trio - - -async def foo_open_nursery(): - async with trio.open_nursery() as _: - yield 1 # error: 8 - - -@asynccontextmanager -async def foo_open_nursery_contextmanager(): - async with trio.open_nursery() as _: - yield 1 # safe diff --git a/tests/eval_files/async111.py b/tests/eval_files/async111.py index 2748460..e40d292 100644 --- a/tests/eval_files/async111.py +++ b/tests/eval_files/async111.py @@ -3,7 +3,6 @@ # It's possible there's an equivalent asyncio construction/gotcha, but methods are differently named, so this file will not raise any errors # nurseries are named taskgroups in asyncio/anyio # ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR from typing import Any import trio diff --git a/tests/eval_files/async111_anyio.py b/tests/eval_files/async111_anyio.py deleted file mode 100644 index 7d99fac..0000000 --- a/tests/eval_files/async111_anyio.py +++ /dev/null @@ -1,21 +0,0 @@ -# main tests in async111.py -# this only tests anyio.create_task_group in particular -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR -# TRIO_NO_ERROR - - -import anyio - - -async def bar(*args): ... - - -async def foo(): - async with anyio.create_task_group() as tg: - with open("") as f: - await tg.start(bar, f) # error: 32, lineno-1, lineno-2, "f", "start" - tg.start_soon(bar, f) # error: 31, lineno-2, lineno-3, "f", "start_soon" - - # create_task does not exist in anyio, but gets errors anyway - tg.create_task(bar(f)) # type: ignore[attr-defined] # error: 31, lineno-5, lineno-6, "f", "create_task" diff --git a/tests/eval_files/async112.py b/tests/eval_files/async112.py index 687443d..8657973 100644 --- a/tests/eval_files/async112.py +++ b/tests/eval_files/async112.py @@ -1,7 +1,6 @@ # type: ignore # ASYNC112: Nursery body with only a call to nursery.start[_soon] and not passing itself as a parameter can be replaced with a regular function call. # ASYNCIO_NO_ERROR -# ANYIO_NO_ERROR import functools from functools import partial diff --git a/tests/eval_files/async112_anyio.py b/tests/eval_files/async112_anyio.py deleted file mode 100644 index 52cf887..0000000 --- a/tests/eval_files/async112_anyio.py +++ /dev/null @@ -1,28 +0,0 @@ -# main tests in async112.py -# this only tests anyio.create_task_group in particular -# BASE_LIBRARY anyio -# ASYNCIO_NO_ERROR -# TRIO_NO_ERROR - -import anyio - - -async def bar(*args): ... - - -async def foo(): - async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup" - await tg.start_soon(bar()) - - async with anyio.create_task_group() as tg: - await tg.start(bar(tg)) - - async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup" - tg.start_soon(bar()) - - async with anyio.create_task_group() as tg: - tg.start_soon(bar(tg)) - - # will not trigger on create_task - async with anyio.create_task_group() as tg: - tg.create_task(bar()) # type: ignore[attr-defined] diff --git a/tests/eval_files/async112_asyncio.py b/tests/eval_files/async112_asyncio.py index d29550c..c0c4b79 100644 --- a/tests/eval_files/async112_asyncio.py +++ b/tests/eval_files/async112_asyncio.py @@ -13,7 +13,7 @@ async def bar(*args): ... async def foo(): - async with asyncio.TaskGroup() as tg: # error: 15, "tg", "taskgroup" + async with asyncio.TaskGroup() as tg: # error: 15, "tg", "task group" tg.create_task(bar()) async with asyncio.TaskGroup() as tg: diff --git a/tests/eval_files/async113.py b/tests/eval_files/async113.py index 22fedc2..bd45446 100644 --- a/tests/eval_files/async113.py +++ b/tests/eval_files/async113.py @@ -7,33 +7,50 @@ import trio # set base library to anyio, so we can replace anyio->asyncio and get correct errors -# BASE_LIBRARY anyio +# BASE_LIBRARY trio +# ASYNCIO_NO_ERROR -# NOTRIO - replacing anyio->trio would give mismatching errors. -# This file tests basic trio errors, and async113_trio checks trio-specific errors +# This file tests basic errors, and async113_trio_anyio checks errors not compatible +# with asyncio + + +async def my_startable(task_status: trio.TaskStatus[object] = trio.TASK_STATUS_IGNORED): + task_status.started() + await trio.lowlevel.checkpoint() @asynccontextmanager async def foo(): - with trio.open_nursery() as bar: - bar.start_soon(trio.run_process) # ASYNC113: 8 + # we don't check for `async with` + with trio.open_nursery() as bar: # type: ignore[attr-defined] + bar.start_soon(my_startable) # ASYNC113: 8 async with trio.open_nursery() as bar: - bar.start_soon(trio.run_process) # ASYNC113: 8 + bar.start_soon(my_startable) # ASYNC113: 8 boo: trio.Nursery = ... # type: ignore - boo.start_soon(trio.run_process) # ASYNC113: 4 + boo.start_soon(my_startable) # ASYNC113: 4 - boo_anyio: anyio.TaskGroup = ... # type: ignore - # false alarm - anyio.run_process is not startable - # (nor is asyncio.run_process) - boo_anyio.start_soon(anyio.run_process) # ASYNC113: 4 + # silence type errors + serve = run_process = serve_listeners = serve_tcp = serve_ssl_over_tcp = ( + my_startable + ) + + # we also trigger if they're a standalone name, assuming that this is + # a wrapper. (or they've ignored the error from doing `from trio import run_process`) + boo.start_soon(serve) # error: 4 + boo.start_soon(run_process) # error: 4 + boo.start_soon(serve_listeners) # error: 4 + boo.start_soon(serve_tcp) # error: 4 + boo.start_soon(serve_ssl_over_tcp) # error: 4 yield -async def my_startable(task_status: trio.TaskStatus[object] = trio.TASK_STATUS_IGNORED): - task_status.started() - await trio.lowlevel.checkpoint() +# we don't type-track [trio/anyio].DTLSEndpoint, so we trigger +# on *.serve +@asynccontextmanager +async def foo_serve(nursey: trio.Nursery, thing: object): + nursey.start_soon(thing.serve) # ASYNC113: 4 # name of variable being [xxx.]nursery triggers it @@ -112,6 +129,6 @@ async def foo_nested_sync_def(): with trio.open_nursery() as bar: def non_async_func(): - bar.start_soon(trio.run_process) + bar.start_soon(my_startable) yield diff --git a/tests/eval_files/async113_anyio.py b/tests/eval_files/async113_anyio.py deleted file mode 100644 index acbe7dd..0000000 --- a/tests/eval_files/async113_anyio.py +++ /dev/null @@ -1,22 +0,0 @@ -# mypy: disable-error-code="arg-type" -# ARG --startable-in-context-manager=my_startable -from contextlib import asynccontextmanager - -import anyio - - -async def my_startable( - task_status: anyio.abc.TaskStatus[object] = anyio.TASK_STATUS_IGNORED, -): - task_status.started() - await anyio.lowlevel.checkpoint() - - -@asynccontextmanager -async def foo(): - # create_task_group only exists in anyio - async with anyio.create_task_group() as bar_tg: - bar_tg.start_soon(my_startable) # ASYNC113: 8 - # false alarm - anyio.run_process is not startable - bar_tg.start_soon(anyio.run_process) # ASYNC113: 8 - yield diff --git a/tests/eval_files/async113_trio.py b/tests/eval_files/async113_trio.py index 3d50c90..c538b43 100644 --- a/tests/eval_files/async113_trio.py +++ b/tests/eval_files/async113_trio.py @@ -12,14 +12,9 @@ # ARG --startable-in-context-manager=custom_startable_function -# ANYIO_NO_ERROR - anyio uses TaskGroups. Checked in async113.py & async113_anyio.py # ASYNCIO_NO_ERROR - asyncio uses TaskGroups. Checked in async113.py - - -@asynccontextmanager -async def custom_startable_externally_tested(): - nursery.start_soon(custom_startable_function) # error: 4 - yield +# We previously errored on anyio.run_process etc +# ANYIO_NO_ERROR @asynccontextmanager @@ -91,8 +86,9 @@ async def __aenter__(self): serve = run_process = myfun = anything # error if name shared with trio - nursery.start_soon(serve) # error: 8 - nursery.start_soon(run_process) # error: 8 + # these error on both trio & anyio, so moved to async113.py + # nursery.start_soon(serve) # error: 8 + # nursery.start_soon(run_process) # error: 8 # don't error if a startable name is a parameter or module nursery.start_soon(myfun(serve=None)) nursery.start_soon(serve.myfun) diff --git a/tests/eval_files/async121.py b/tests/eval_files/async121.py index b55f21b..f285597 100644 --- a/tests/eval_files/async121.py +++ b/tests/eval_files/async121.py @@ -1,5 +1,4 @@ # ASYNCIO_NO_ERROR # checked in async121_asyncio.py -# ANYIO_NO_ERROR # checked in async121_anyio.py import trio from typing import Any diff --git a/tests/eval_files/async121_anyio.py b/tests/eval_files/async121_anyio.py index 12be0ae..2a9a322 100644 --- a/tests/eval_files/async121_anyio.py +++ b/tests/eval_files/async121_anyio.py @@ -1,5 +1,4 @@ # ASYNCIO_NO_ERROR # checked in async121_asyncio.py -# TRIO_NO_ERROR # checked in async121.py # BASE_LIBRARY anyio import anyio diff --git a/tests/eval_files/async124.py b/tests/eval_files/async124.py index 11db9ba..058f0f3 100644 --- a/tests/eval_files/async124.py +++ b/tests/eval_files/async124.py @@ -7,7 +7,8 @@ # 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. -# NOAUTOFIX # all errors get "fixed" except for foo_fix_no_subfix +# AUTOFIX # all errors get "fixed" except for foo_fix_no_subfix in async124_no_autofix.py +# ASYNCIO_NO_AUTOFIX from typing import Any, overload from pytest import fixture @@ -98,10 +99,8 @@ async def foo_fix(my_async_fixture): print("blah") -# but @fixture with no params can be converted to sync -@fixture -async def foo_fix_no_subfix(): # ASYNC124: 0 - print("blah") +# @fixture with no params can be converted to sync +# see async124_no_autofix.py async def default_value(): diff --git a/tests/eval_files/async124_no_autofix.py b/tests/eval_files/async124_no_autofix.py new file mode 100644 index 0000000..8bc95d0 --- /dev/null +++ b/tests/eval_files/async124_no_autofix.py @@ -0,0 +1,9 @@ +# ARG --enable=ASYNC124,ASYNC910,ASYNC911 +from pytest import fixture + + +# @fixture with no params can be converted to sync +# 910/911 skips funcs with `@fixture` decorator, so this doesn't get auto"fixed" +@fixture +async def foo_fix_no_subfix(): # ASYNC124: 0 + print("blah") diff --git a/tests/eval_files/async251.py b/tests/eval_files/async251.py index d6c72a2..9da4a10 100644 --- a/tests/eval_files/async251.py +++ b/tests/eval_files/async251.py @@ -1,5 +1,3 @@ -# NOAUTOFIX - import time from time import sleep diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py index 86673b4..02863b6 100644 --- a/tests/test_flake8_async.py +++ b/tests/test_flake8_async.py @@ -102,20 +102,28 @@ def diff_strings(first: str, second: str, /) -> str: # replaces all instances of `original` with `new` in string # unless it's preceded by a `-`, which indicates it's part of a command-line flag -def replace_library( - string: str, - original: str = "trio", - new: str = "anyio", - replace_nursery: bool = False, -) -> str: +def replace_library(string: str, original: str = "trio", new: str = "anyio") -> str: def replace_str(string: str, original: str, new: str) -> str: return re.sub(rf"(?