From fa6bdbbc942913fb6ce72deab02043e34299f52d Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 31 Jan 2025 17:57:19 +0100
Subject: [PATCH 01/10] make ASYNC912 and 913 care about cancel points, and
ignore schedule points
---
docs/changelog.rst | 4 ++++
docs/rules.rst | 5 +++--
docs/usage.rst | 2 +-
flake8_async/__init__.py | 2 +-
flake8_async/visitors/visitor91x.py | 14 ++++++++++----
tests/autofix_files/async913_trio_anyio.py | 12 ++++++++++++
tests/autofix_files/async913_trio_anyio.py.diff | 9 +++++++++
tests/eval_files/async912.py | 9 ++++++++-
tests/eval_files/async913_trio_anyio.py | 11 +++++++++++
tests/test_flake8_async.py | 9 ++++++++-
tox.ini | 1 -
11 files changed, 67 insertions(+), 11 deletions(-)
create mode 100644 tests/autofix_files/async913_trio_anyio.py
create mode 100644 tests/autofix_files/async913_trio_anyio.py.diff
create mode 100644 tests/eval_files/async913_trio_anyio.py
diff --git a/docs/changelog.rst b/docs/changelog.rst
index f1658de..9bd7e36 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,10 @@ Changelog
`CalVer, YY.month.patch `_
+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.
+
25.1.1
=======
- Add :ref:`ASYNC124 ` async-function-could-be-sync
diff --git a/docs/rules.rst b/docs/rules.rst
index b2bef0b..9d32e14 100644
--- a/docs/rules.rst
+++ b/docs/rules.rst
@@ -197,12 +197,13 @@ _`ASYNC911` : async-generator-no-checkpoint
Exit, ``yield`` or ``return`` from async iterable with no guaranteed :ref:`checkpoint` since possible function entry (``yield`` or function definition).
_`ASYNC912` : cancel-scope-no-guaranteed-checkpoint
- A timeout/cancelscope has :ref:`checkpoints `, but they're not guaranteed to run.
- Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no checkpoint at all.
+ A timeout/cancelscope has :ref:`cancel points `, but they're not guaranteed to run.
+ Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no cancel point at all.
It instead shares logic with `ASYNC910`_ and `ASYNC911`_ for parsing conditionals and branches.
_`ASYNC913` : indefinite-loop-no-guaranteed-checkpoint
An indefinite loop (e.g. ``while True``) has no guaranteed :ref:`checkpoint `. This could potentially cause a deadlock.
+ This will also error if there's no guaranteed :ref:`cancel point`, where even though it won't deadlock the loop might become an uncancelable dry-run loop.
.. _autofix-support:
diff --git a/docs/usage.rst b/docs/usage.rst
index d72e812..e6e116f 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.1.1
+ rev: 25.2.1
hooks:
- id: flake8-async
# args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]
diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py
index 405aa28..aa715cb 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.1.1"
+__version__ = "25.2.1"
# taken from https://github.com/Zac-HD/shed
diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py
index 0036c79..b664e64 100644
--- a/flake8_async/visitors/visitor91x.py
+++ b/flake8_async/visitors/visitor91x.py
@@ -354,10 +354,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
"on line {1.lineno}."
),
"ASYNC912": (
- "CancelScope with no guaranteed checkpoint. This makes it potentially "
+ "CancelScope with no guaranteed cancel point. This makes it potentially "
"impossible to cancel."
),
- "ASYNC913": ("Indefinite loop with no guaranteed checkpoint."),
+ "ASYNC913": ("Indefinite loop with no guaranteed cancel points."),
"ASYNC100": (
"{0}.{1} context contains no checkpoints, remove the context or add"
" `await {0}.lowlevel.checkpoint()`."
@@ -401,10 +401,16 @@ def checkpoint_cancel_point(self) -> None:
self.taskgroup_has_start_soon.clear()
def checkpoint_schedule_point(self) -> None:
- self.uncheckpointed_statements = set()
+ # ASYNC912&ASYNC913 only cares about cancel points, so don't remove
+ # them if we only do a schedule point
+ self.uncheckpointed_statements = {
+ s
+ for s in self.uncheckpointed_statements
+ if isinstance(s, ArtificialStatement)
+ }
def checkpoint(self) -> None:
- self.checkpoint_schedule_point()
+ self.uncheckpointed_statements = set()
self.checkpoint_cancel_point()
def checkpoint_statement(self) -> cst.SimpleStatementLine:
diff --git a/tests/autofix_files/async913_trio_anyio.py b/tests/autofix_files/async913_trio_anyio.py
new file mode 100644
index 0000000..76157ed
--- /dev/null
+++ b/tests/autofix_files/async913_trio_anyio.py
@@ -0,0 +1,12 @@
+# ARG --enable=ASYNC913
+# AUTOFIX
+# ASYNCIO_NO_ERROR
+
+import trio
+
+
+async def nursery_no_cancel_point():
+ while True: # ASYNC913: 4
+ await trio.lowlevel.checkpoint()
+ async with trio.open_nursery():
+ pass
diff --git a/tests/autofix_files/async913_trio_anyio.py.diff b/tests/autofix_files/async913_trio_anyio.py.diff
new file mode 100644
index 0000000..dc16728
--- /dev/null
+++ b/tests/autofix_files/async913_trio_anyio.py.diff
@@ -0,0 +1,9 @@
+---
++++
+@@ x,5 x,6 @@
+
+ async def nursery_no_cancel_point():
+ while True: # ASYNC913: 4
++ await trio.lowlevel.checkpoint()
+ async with trio.open_nursery():
+ pass
diff --git a/tests/eval_files/async912.py b/tests/eval_files/async912.py
index 1813ff2..040c39c 100644
--- a/tests/eval_files/async912.py
+++ b/tests/eval_files/async912.py
@@ -125,7 +125,7 @@ def customWrapper(a: T) -> T:
with (res := trio.fail_at(10)):
...
# but saving with `as` does
- with trio.fail_at(10) as res: # ASYNC912: 9
+ with trio.fail_at(10) as res2: # ASYNC912: 9
if bar():
await trio.lowlevel.checkpoint()
@@ -189,3 +189,10 @@ async def check_yield_logic():
if bar():
await trio.lowlevel.checkpoint()
yield
+
+
+async def nursery_no_cancel_point():
+ with trio.move_on_after(10): # ASYNC912: 9
+ async with trio.open_nursery():
+ if bar():
+ await trio.lowlevel.checkpoint()
diff --git a/tests/eval_files/async913_trio_anyio.py b/tests/eval_files/async913_trio_anyio.py
new file mode 100644
index 0000000..28fc0a1
--- /dev/null
+++ b/tests/eval_files/async913_trio_anyio.py
@@ -0,0 +1,11 @@
+# ARG --enable=ASYNC913
+# AUTOFIX
+# ASYNCIO_NO_ERROR
+
+import trio
+
+
+async def nursery_no_cancel_point():
+ while True: # ASYNC913: 4
+ async with trio.open_nursery():
+ pass
diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py
index 733ec0c..0895764 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -103,7 +103,14 @@ 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") -> str:
- return re.sub(rf"(? str:
+ return re.sub(rf"(?
Date: Fri, 31 Jan 2025 18:13:18 +0100
Subject: [PATCH 02/10] fix doc reference
---
docs/rules.rst | 2 +-
tox.ini | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/rules.rst b/docs/rules.rst
index 9d32e14..974d99c 100644
--- a/docs/rules.rst
+++ b/docs/rules.rst
@@ -203,7 +203,7 @@ _`ASYNC912` : cancel-scope-no-guaranteed-checkpoint
_`ASYNC913` : indefinite-loop-no-guaranteed-checkpoint
An indefinite loop (e.g. ``while True``) has no guaranteed :ref:`checkpoint `. This could potentially cause a deadlock.
- This will also error if there's no guaranteed :ref:`cancel point`, where even though it won't deadlock the loop might become an uncancelable dry-run loop.
+ This will also error if there's no guaranteed :ref:`cancel point `, where even though it won't deadlock the loop might become an uncancelable dry-run loop.
.. _autofix-support:
diff --git a/tox.ini b/tox.ini
index b7579d7..2bcca72 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,6 +30,7 @@ allowlist_externals = make
changedir = docs
skip_install = True
commands =
+ make clean
make html
# Settings for other tools
From 88d2e1f93bef19bc80d07cd7ba32d6d1868a00e5 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Fri, 31 Jan 2025 18:50:12 +0100
Subject: [PATCH 03/10] all tests passing :tada:
---
flake8_async/visitors/visitors.py | 20 ++++++++++++--------
tests/autofix_files/async100_anyio.py | 1 -
tests/autofix_files/async100_trio.py | 1 -
tests/eval_files/async100_anyio.py | 1 -
tests/eval_files/async100_trio.py | 1 -
tests/eval_files/async101.py | 15 +++++++++++----
tests/eval_files/async101_anyio.py | 1 -
tests/eval_files/async101_trio.py | 1 -
tests/eval_files/async111.py | 1 -
tests/eval_files/async111_anyio.py | 1 -
tests/eval_files/async112.py | 1 -
tests/eval_files/async112_anyio.py | 5 ++---
tests/eval_files/async112_asyncio.py | 2 +-
tests/eval_files/async113_trio.py | 1 -
tests/eval_files/async121.py | 1 -
tests/eval_files/async121_anyio.py | 1 -
tests/test_flake8_async.py | 3 +++
17 files changed, 29 insertions(+), 28 deletions(-)
diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py
index 54f14cf..2a8d824 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
@@ -213,12 +213,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",
+ )
)
)
diff --git a/tests/autofix_files/async100_anyio.py b/tests/autofix_files/async100_anyio.py
index 80516cf..9671661 100644
--- a/tests/autofix_files/async100_anyio.py
+++ b/tests/autofix_files/async100_anyio.py
@@ -1,6 +1,5 @@
# AUTOFIX
# BASE_LIBRARY anyio
-# NOTRIO # trio.create_task_group doesn't exist
# ASYNCIO_NO_ERROR
import anyio
diff --git a/tests/autofix_files/async100_trio.py b/tests/autofix_files/async100_trio.py
index ed61139..875a8ff 100644
--- a/tests/autofix_files/async100_trio.py
+++ b/tests/autofix_files/async100_trio.py
@@ -1,6 +1,5 @@
# AUTOFIX
# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist
-# NOANYIO # anyio.open_nursery doesn't exist
import trio
diff --git a/tests/eval_files/async100_anyio.py b/tests/eval_files/async100_anyio.py
index f274969..edf1ac6 100644
--- a/tests/eval_files/async100_anyio.py
+++ b/tests/eval_files/async100_anyio.py
@@ -1,6 +1,5 @@
# AUTOFIX
# BASE_LIBRARY anyio
-# NOTRIO # trio.create_task_group doesn't exist
# ASYNCIO_NO_ERROR
import anyio
diff --git a/tests/eval_files/async100_trio.py b/tests/eval_files/async100_trio.py
index d722185..d3d263c 100644
--- a/tests/eval_files/async100_trio.py
+++ b/tests/eval_files/async100_trio.py
@@ -1,6 +1,5 @@
# AUTOFIX
# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist
-# NOANYIO # anyio.open_nursery doesn't exist
import trio
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
index 9f16b8c..0108ad2 100644
--- a/tests/eval_files/async101_anyio.py
+++ b/tests/eval_files/async101_anyio.py
@@ -1,5 +1,4 @@
# BASE_LIBRARY anyio
-# TRIO_NO_ERROR
# ASYNCIO_NO_ERROR
import anyio
diff --git a/tests/eval_files/async101_trio.py b/tests/eval_files/async101_trio.py
index 3ceda36..f146a32 100644
--- a/tests/eval_files/async101_trio.py
+++ b/tests/eval_files/async101_trio.py
@@ -1,5 +1,4 @@
# ASYNCIO_NO_ERROR
-# ANYIO_NO_ERROR
from contextlib import asynccontextmanager
diff --git a/tests/eval_files/async111.py b/tests/eval_files/async111.py
index cf29a05..a0ebc08 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
index 7d99fac..01f80c6 100644
--- a/tests/eval_files/async111_anyio.py
+++ b/tests/eval_files/async111_anyio.py
@@ -2,7 +2,6 @@
# this only tests anyio.create_task_group in particular
# BASE_LIBRARY anyio
# ASYNCIO_NO_ERROR
-# TRIO_NO_ERROR
import anyio
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
index 52cf887..5b74f9d 100644
--- a/tests/eval_files/async112_anyio.py
+++ b/tests/eval_files/async112_anyio.py
@@ -2,7 +2,6 @@
# this only tests anyio.create_task_group in particular
# BASE_LIBRARY anyio
# ASYNCIO_NO_ERROR
-# TRIO_NO_ERROR
import anyio
@@ -11,13 +10,13 @@ async def bar(*args): ...
async def foo():
- async with anyio.create_task_group() as tg: # error: 15, "tg", "taskgroup"
+ async with anyio.create_task_group() as tg: # error: 15, "tg", "task group"
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"
+ async with anyio.create_task_group() as tg: # error: 15, "tg", "task group"
tg.start_soon(bar())
async with anyio.create_task_group() as tg:
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_trio.py b/tests/eval_files/async113_trio.py
index 3d50c90..983a0e0 100644
--- a/tests/eval_files/async113_trio.py
+++ b/tests/eval_files/async113_trio.py
@@ -12,7 +12,6 @@
# 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
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/test_flake8_async.py b/tests/test_flake8_async.py
index 0895764..6a00fe0 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -108,8 +108,11 @@ def replace_str(string: str, original: str, new: str) -> str:
if original == "trio" and new == "anyio":
string = replace_str(string, "trio.open_nursery", "anyio.create_task_group")
+ string = replace_str(string, '"nursery"', '"task group"')
+ string = replace_str(string, "nursery", "task_group")
elif original == "anyio" and new == "trio":
string = replace_str(string, "anyio.create_task_group", "trio.open_nursery")
+ string = replace_str(string, "task group", "nursery")
return replace_str(string, original, new)
From 3fb511881719feb6b090571f091492eefcbe2a1d Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 3 Feb 2025 13:21:56 +0100
Subject: [PATCH 04/10] delete now redundant eval files, fix async113 false
alarm on [not trio].serve_listeners etc
---
flake8_async/visitors/visitors.py | 17 +++--
tests/autofix_files/async100.py | 74 ++++++++++++++++++++++
tests/autofix_files/async100.py.diff | 69 ++++++++++++++++++--
tests/autofix_files/async100_anyio.py | 25 --------
tests/autofix_files/async100_anyio.py.diff | 23 -------
tests/autofix_files/async100_trio.py | 74 ----------------------
tests/autofix_files/async100_trio.py.diff | 57 -----------------
tests/eval_files/async100.py | 74 ++++++++++++++++++++++
tests/eval_files/async100_anyio.py | 25 --------
tests/eval_files/async100_trio.py | 74 ----------------------
tests/eval_files/async101_anyio.py | 9 ---
tests/eval_files/async101_trio.py | 16 -----
tests/eval_files/async111_anyio.py | 20 ------
tests/eval_files/async112_anyio.py | 27 --------
tests/eval_files/async113.py | 47 +++++++++-----
tests/eval_files/async113_anyio.py | 22 -------
tests/eval_files/async113_trio.py | 13 ++--
tests/test_flake8_async.py | 2 +
18 files changed, 264 insertions(+), 404 deletions(-)
delete mode 100644 tests/autofix_files/async100_anyio.py
delete mode 100644 tests/autofix_files/async100_anyio.py.diff
delete mode 100644 tests/autofix_files/async100_trio.py
delete mode 100644 tests/autofix_files/async100_trio.py.diff
delete mode 100644 tests/eval_files/async100_anyio.py
delete mode 100644 tests/eval_files/async100_trio.py
delete mode 100644 tests/eval_files/async101_anyio.py
delete mode 100644 tests/eval_files/async101_trio.py
delete mode 100644 tests/eval_files/async111_anyio.py
delete mode 100644 tests/eval_files/async112_anyio.py
delete mode 100644 tests/eval_files/async113_anyio.py
diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py
index 2a8d824..02c4948 100644
--- a/flake8_async/visitors/visitors.py
+++ b/flake8_async/visitors/visitors.py
@@ -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
@@ -233,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,
)
):
@@ -258,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 9671661..0000000
--- a/tests/autofix_files/async100_anyio.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# AUTOFIX
-# BASE_LIBRARY anyio
-# 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 875a8ff..0000000
--- a/tests/autofix_files/async100_trio.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# AUTOFIX
-# ASYNCIO_NO_ERROR # asyncio.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/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 edf1ac6..0000000
--- a/tests/eval_files/async100_anyio.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# AUTOFIX
-# BASE_LIBRARY anyio
-# 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 d3d263c..0000000
--- a/tests/eval_files/async100_trio.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# AUTOFIX
-# ASYNCIO_NO_ERROR # asyncio.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_anyio.py b/tests/eval_files/async101_anyio.py
deleted file mode 100644
index 0108ad2..0000000
--- a/tests/eval_files/async101_anyio.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# BASE_LIBRARY anyio
-# 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 f146a32..0000000
--- a/tests/eval_files/async101_trio.py
+++ /dev/null
@@ -1,16 +0,0 @@
-# ASYNCIO_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_anyio.py b/tests/eval_files/async111_anyio.py
deleted file mode 100644
index 01f80c6..0000000
--- a/tests/eval_files/async111_anyio.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# main tests in async111.py
-# this only tests anyio.create_task_group in particular
-# BASE_LIBRARY anyio
-# ASYNCIO_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_anyio.py b/tests/eval_files/async112_anyio.py
deleted file mode 100644
index 5b74f9d..0000000
--- a/tests/eval_files/async112_anyio.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# main tests in async112.py
-# this only tests anyio.create_task_group in particular
-# BASE_LIBRARY anyio
-# ASYNCIO_NO_ERROR
-
-import anyio
-
-
-async def bar(*args): ...
-
-
-async def foo():
- async with anyio.create_task_group() as tg: # error: 15, "tg", "task group"
- 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", "task group"
- 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/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 983a0e0..c538b43 100644
--- a/tests/eval_files/async113_trio.py
+++ b/tests/eval_files/async113_trio.py
@@ -13,12 +13,8 @@
# ARG --startable-in-context-manager=custom_startable_function
# 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
@@ -90,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/test_flake8_async.py b/tests/test_flake8_async.py
index 6a00fe0..728968d 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -110,9 +110,11 @@ def replace_str(string: str, original: str, new: str) -> str:
string = replace_str(string, "trio.open_nursery", "anyio.create_task_group")
string = replace_str(string, '"nursery"', '"task group"')
string = replace_str(string, "nursery", "task_group")
+ string = replace_str(string, "Nursery", "TaskGroup")
elif original == "anyio" and new == "trio":
string = replace_str(string, "anyio.create_task_group", "trio.open_nursery")
string = replace_str(string, "task group", "nursery")
+ string = replace_str(string, "TaskGroup", "Nursery")
return replace_str(string, original, new)
From d3d18bb312e11450903fbecb3cf326b50f827cf5 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Mon, 3 Feb 2025 16:05:39 +0100
Subject: [PATCH 05/10] clean up implementation
---
tests/test_flake8_async.py | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py
index 728968d..5be4865 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -106,15 +106,24 @@ def replace_library(string: str, original: str = "trio", new: str = "anyio") ->
def replace_str(string: str, original: str, new: str) -> str:
return re.sub(rf"(?
Date: Mon, 3 Feb 2025 17:18:30 +0100
Subject: [PATCH 06/10] lol that was dumb
---
tests/test_flake8_async.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/test_flake8_async.py b/tests/test_flake8_async.py
index 5be4865..02863b6 100644
--- a/tests/test_flake8_async.py
+++ b/tests/test_flake8_async.py
@@ -115,7 +115,7 @@ def replace_str(string: str, original: str, new: str) -> str:
("Nursery", "TaskGroup"),
)
- if sorted((original, new)) == ["trio", "anyio"]:
+ if sorted((original, new)) == ["anyio", "trio"]:
for trio_, anyio_ in replacements:
if original == "trio":
from_ = trio_
From 491e21bb359acee50be0a4b96ba82edf88c4d837 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 4 Feb 2025 11:34:04 +0100
Subject: [PATCH 07/10] merge origin/main
---
.pre-commit-config.yaml | 8 ++++----
flake8_async/visitors/visitor2xx.py | 6 ++++--
flake8_async/visitors/visitor91x.py | 2 +-
flake8_async/visitors/visitors.py | 12 ++++++++----
tests/eval_files/async111.py | 1 -
5 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 5f71152..4aef83a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,13 +9,13 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.9.3
+ rev: v0.9.4
hooks:
- id: ruff
args: [--fix]
- repo: https://github.com/psf/black
- rev: 24.10.0
+ rev: 25.1.0
hooks:
- id: black
args: [--preview]
@@ -45,7 +45,7 @@ repos:
exclude: tests/eval_files/.*_py311.py
- repo: https://github.com/RobertCraigie/pyright-python
- rev: v1.1.392.post0
+ rev: v1.1.393
hooks:
- id: pyright
# ignore warnings about new version being available, no other warnings
@@ -66,7 +66,7 @@ repos:
- trio
- repo: https://github.com/codespell-project/codespell
- rev: v2.4.0
+ rev: v2.4.1
hooks:
- id: codespell
additional_dependencies:
diff --git a/flake8_async/visitors/visitor2xx.py b/flake8_async/visitors/visitor2xx.py
index b49fe72..cd416d1 100644
--- a/flake8_async/visitors/visitor2xx.py
+++ b/flake8_async/visitors/visitor2xx.py
@@ -186,7 +186,9 @@ class Visitor22X(Visitor200):
"Sync call {} in async function, use "
"`asyncio.create_subprocess_[exec/shell]."
),
- "ASYNC222": "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`.",
+ "ASYNC222": (
+ "Sync call {} in async function, wrap in `{}.to_thread.run_sync()`."
+ ),
"ASYNC222_asyncio": (
"Sync call {} in async function, use `asyncio.loop.run_in_executor`."
),
@@ -397,7 +399,7 @@ def visit_Call(self, node: ast.Call):
@error_class
class Visitor25X(Visitor200):
error_codes: Mapping[str, str] = {
- "ASYNC250": ("Blocking sync call `input()` in async function. Wrap in `{}`."),
+ "ASYNC250": "Blocking sync call `input()` in async function. Wrap in `{}`.",
"ASYNC251": (
"Blocking sync call `time.sleep(...)` in async function."
" Use `await {}.sleep(...)`."
diff --git a/flake8_async/visitors/visitor91x.py b/flake8_async/visitors/visitor91x.py
index b664e64..823e8f6 100644
--- a/flake8_async/visitors/visitor91x.py
+++ b/flake8_async/visitors/visitor91x.py
@@ -357,7 +357,7 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
"CancelScope with no guaranteed cancel point. This makes it potentially "
"impossible to cancel."
),
- "ASYNC913": ("Indefinite loop with no guaranteed cancel points."),
+ "ASYNC913": "Indefinite loop with no guaranteed cancel points.",
"ASYNC100": (
"{0}.{1} context contains no checkpoints, remove the context or add"
" `await {0}.lowlevel.checkpoint()`."
diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py
index 02c4948..92ed2b1 100644
--- a/flake8_async/visitors/visitors.py
+++ b/flake8_async/visitors/visitors.py
@@ -332,8 +332,10 @@ def visit_Call(self, node: ast.Call):
@error_class
class Visitor119(Flake8AsyncVisitor):
error_codes: Mapping[str, str] = {
- "ASYNC119": "Yield in contextmanager in async generator might not trigger"
- " cleanup. Use `@asynccontextmanager` or refactor."
+ "ASYNC119": (
+ "Yield in contextmanager in async generator might not trigger"
+ " cleanup. Use `@asynccontextmanager` or refactor."
+ )
}
def __init__(self, *args: Any, **kwargs: Any):
@@ -503,8 +505,10 @@ def leave_IfExp_test(self, node: cst.IfExp):
@disabled_by_default
class Visitor900(Flake8AsyncVisitor):
error_codes: Mapping[str, str] = {
- "ASYNC900": "Async generator not allowed, unless transformed "
- "by a known decorator (one of: {})."
+ "ASYNC900": (
+ "Async generator not allowed, unless transformed "
+ "by a known decorator (one of: {})."
+ )
}
def __init__(self, *args: Any, **kwargs: Any):
diff --git a/tests/eval_files/async111.py b/tests/eval_files/async111.py
index a0ebc08..e40d292 100644
--- a/tests/eval_files/async111.py
+++ b/tests/eval_files/async111.py
@@ -8,7 +8,6 @@
import trio
import trio as noterror
-
# shed/black breaks up a *ton* of lines since adding more detailed error messages, so
# disable formatting to avoid having to adjust a ton of line references
# fmt: off
From 0c697809aaa25857e9d5eb1336a3016d44ebd9ae Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 4 Feb 2025 11:34:50 +0100
Subject: [PATCH 08/10] get rid of two NOAUTOFIX markers
---
tests/autofix_files/async124.py | 159 ++++++++++++++++++++++++
tests/autofix_files/async124.py.diff | 86 +++++++++++++
tests/eval_files/async124.py | 9 +-
tests/eval_files/async124_no_autofix.py | 9 ++
tests/eval_files/async251.py | 2 -
5 files changed, 258 insertions(+), 7 deletions(-)
create mode 100644 tests/autofix_files/async124.py
create mode 100644 tests/autofix_files/async124.py.diff
create mode 100644 tests/eval_files/async124_no_autofix.py
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/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
From c66440e0378484e74c12dbe2776aba90dcde5615 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 4 Feb 2025 11:40:44 +0100
Subject: [PATCH 09/10] changelog & version
---
docs/changelog.rst | 4 ++++
docs/usage.rst | 2 +-
flake8_async/__init__.py | 2 +-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 9bd7e36..80727b0 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -4,6 +4,10 @@ Changelog
`CalVer, YY.month.patch `_
+25.2.2
+=======
+- :ref:`ASYNC133 ` 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
From 4e0964c77375f131b1612a02517b21d76fe14ea0 Mon Sep 17 00:00:00 2001
From: jakkdl
Date: Tue, 4 Feb 2025 11:58:06 +0100
Subject: [PATCH 10/10] very nice test to catch this typo :)
---
docs/changelog.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 80727b0..c8f5083 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -6,7 +6,7 @@ Changelog
25.2.2
=======
-- :ref:`ASYNC133 ` 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).
+- :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
=======