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"(?