Skip to content

Commit 126eab4

Browse files
authored
add asyncio/anyio taskgroup support to async111&112 (#241)
* add asyncio/anyio taskgroup support to async111, update contributing.md * add asyncio/anyio taskgroup support to async112
1 parent 504b636 commit 126eab4

File tree

9 files changed

+148
-21
lines changed

9 files changed

+148
-21
lines changed

CONTRIBUTING.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,15 @@ You can instead of `error` specify the error code.
4444
With `# ARG` lines you can also specify command-line arguments that should be passed to the plugin when parsing a file. Can be specified multiple times for several different arguments.
4545
Generated tests will by default `--select` the error code of the file, which will enable any visitors that can generate that code (and if those visitors can raise other codes they might be raised too). This can be overridden by adding an `# ARG --select=...` line.
4646

47-
### `# NOANYIO` and `# NOTRIO`
48-
Eval files are also evaluated where ~all instances of "trio" is replaced with "anyio" and `--anyio` is prepended to the argument list, to check for compatibility with the [anyio](https://github.com/agronholm/anyio) library - unless they are marked with `# NOANYIO`.
49-
If a file is marked with `# NOTRIO` it will not replace instances of "trio" with "anyio", and the file will not be run by the normal `eval_files` test generator.
50-
#### `# ANYIO_NO_ERROR`
51-
A file which is marked with this will ignore all `# error` or `# TRIO...` comments when running with anyio. Use when an error is trio-specific and replacing "trio" with "anyio" should silence all errors.
47+
### Library parametrization
48+
Eval files are evaluated with each supported library. It does this by replacing all instances of the `BASE_LIBRARY` ("trio" by default) with the two other libraries, and setting the corresponding flag (`--anyio` or `--asyncio`).
49+
### `# BASE_LIBRARY anyio` / `# BASE_LIBRARY asyncio`
50+
Defaults to `trio`. Used to specify the primary library an eval file is testing.
51+
52+
#### `# ANYIO_NO_ERROR`, `# TRIO_NO_ERROR`, `# ASYNCIO_NO_ERROR`
53+
A file which is marked with this will ignore all `# error` or `# TRIO...` comments when running with anyio. Use when an error is library-specific and replacing all instances means the file should no longer raise any errors.
54+
### `# NOANYIO`, `# NOTRIO`, `#NOASYNCIO`
55+
Disables checking a file with the specified library. Should be used somewhat sparingly, and always have a comment motivating its use.
5256

5357
## Running pytest outside tox
5458
If you don't want to bother with tox to quickly test stuff, you'll need to install the following dependencies:

flake8_async/visitors/visitor111.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@
1212
from collections.abc import Mapping
1313

1414

15+
def is_nursery_like(node: ast.expr) -> bool:
16+
return bool(
17+
get_matching_call(node, "open_nursery", base="trio")
18+
or get_matching_call(node, "create_task_group", base="anyio")
19+
or get_matching_call(node, "TaskGroup", base="asyncio")
20+
)
21+
22+
1523
@error_class
1624
class Visitor111(Flake8AsyncVisitor):
1725
error_codes: Mapping[str, str] = {
1826
"ASYNC111": (
1927
"variable {2} is usable within the context manager on line {0}, but that "
20-
"will close before nursery opened on line {1} - this is usually a bug. "
21-
"Nurseries should generally be the inner-most context manager."
28+
"will close before nursery/taskgroup opened on line {1} - this is usually "
29+
"a bug. Nursery/TaskGroup should generally be the inner-most context manager."
2230
),
2331
}
2432

@@ -48,8 +56,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
4856
self.TrioContextManager(
4957
item.context_expr.lineno,
5058
item.optional_vars.id,
51-
get_matching_call(item.context_expr, "open_nursery")
52-
is not None,
59+
is_nursery_like(item.context_expr),
5360
)
5461
)
5562

@@ -75,7 +82,7 @@ def visit_Call(self, node: ast.Call):
7582
if (
7683
isinstance(node.func, ast.Attribute)
7784
and isinstance(node.func.value, ast.Name)
78-
and node.func.attr in ("start", "start_soon")
85+
and node.func.attr in ("start", "start_soon", "create_task")
7986
):
8087
self._nursery_call = None
8188
for i, cm in enumerate(self._context_managers):

flake8_async/visitors/visitors.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,29 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
104104
continue
105105
var_name = item.optional_vars.id
106106

107-
# check for trio.open_nursery
107+
# check for trio.open_nursery and anyio.create_task_group
108108
nursery = get_matching_call(
109-
item.context_expr, "open_nursery", base=("trio",)
110-
)
109+
item.context_expr, "open_nursery", base="trio"
110+
) or get_matching_call(item.context_expr, "create_task_group", base="anyio")
111+
start_methods: tuple[str, ...] = ("start", "start_soon")
112+
if nursery is None:
113+
# check for asyncio.TaskGroup
114+
nursery = get_matching_call(
115+
item.context_expr, "TaskGroup", base="asyncio"
116+
)
117+
if nursery is None:
118+
continue
119+
start_methods = ("create_task",)
120+
121+
body_call = node.body[0].value
122+
if isinstance(body_call, ast.Await):
123+
body_call = body_call.value
111124

112125
# `isinstance(..., ast.Call)` is done in get_matching_call
113-
body_call = cast("ast.Call", node.body[0].value)
126+
body_call = cast("ast.Call", body_call)
114127

115128
if (
116-
nursery is not None
117-
and get_matching_call(body_call, "start", "start_soon", base=var_name)
129+
get_matching_call(body_call, *start_methods, base=var_name)
118130
# check for presence of <X> as parameter
119131
and not any(
120132
(isinstance(n, ast.Name) and n.id == var_name)

tests/eval_files/async111.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# type: ignore
22
# ASYNC111: Variable, from context manager opened inside nursery, passed to start[_soon] might be invalidly accessed while in use, due to context manager closing before the nursery. This is usually a bug, and nurseries should generally be the inner-most context manager.
33
# It's possible there's an equivalent asyncio construction/gotcha, but methods are differently named, so this file will not raise any errors
4-
# ASYNCIO_NO_ERROR # no nurseries in asyncio. Though maybe the bug is relevant for TaskGroups
4+
# nurseries are named taskgroups in asyncio/anyio
5+
# ASYNCIO_NO_ERROR
6+
# ANYIO_NO_ERROR
57
from typing import Any
68

79
import trio
@@ -242,3 +244,9 @@ def myfun(nursery, bar):
242244
nursery.start(f)
243245

244246
# fmt: on
247+
248+
# visitor does not care to keep track of the type of nursery/taskgroup, so we
249+
# raise errors on .create_task() even if it doesn't exist in trio.
250+
with trio.open_nursery() as nursery:
251+
with open("") as f:
252+
nursery.create_task(f) # error: 28, line-1, line-2, "f", "create_task"

tests/eval_files/async111_anyio.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# main tests in async111.py
2+
# this only tests anyio.create_task_group in particular
3+
# BASE_LIBRARY anyio
4+
# ASYNCIO_NO_ERROR
5+
# TRIO_NO_ERROR
6+
7+
8+
import anyio
9+
10+
11+
async def bar(*args): ...
12+
13+
14+
async def foo():
15+
async with anyio.create_task_group() as tg:
16+
with open("") as f:
17+
await tg.start(bar, f) # error: 32, lineno-1, lineno-2, "f", "start"
18+
tg.start_soon(bar, f) # error: 31, lineno-2, lineno-3, "f", "start_soon"
19+
20+
# create_task does not exist in anyio, but gets errors anyway
21+
tg.create_task(bar(f)) # type: ignore[attr-defined] # error: 31, lineno-5, lineno-6, "f", "create_task"

tests/eval_files/async111_asyncio.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# main tests in async111.py
2+
# this only tests asyncio.TaskGroup in particular
3+
# BASE_LIBRARY asyncio
4+
# ANYIO_NO_ERROR
5+
# TRIO_NO_ERROR
6+
# TaskGroup introduced in 3.11, we run typechecks with 3.9
7+
# mypy: disable-error-code=attr-defined
8+
9+
10+
import asyncio
11+
12+
13+
async def bar(*args): ...
14+
15+
16+
async def foo():
17+
async with asyncio.TaskGroup() as tg:
18+
with open("") as f:
19+
tg.create_task(bar(f)) # error: 31, lineno-1, lineno-2, "f", "create_task"
20+
21+
# start[_soon] does not exist in asyncio, but gets errors anyway
22+
await tg.start(bar, f) # error: 32, lineno-4, lineno-5, "f", "start"
23+
tg.start_soon(bar, f) # error: 31, lineno-5, lineno-6, "f", "start_soon"

tests/eval_files/async112.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# type: ignore
22
# 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.
3-
# ASYNCIO_NO_ERROR - # TODO: expand check to work with asyncio.TaskGroup
4-
# ANYIO_NO_ERROR - # TODO: expand check to work with anyio.TaskGroup
3+
# ASYNCIO_NO_ERROR
4+
# ANYIO_NO_ERROR
55
import functools
66
from functools import partial
77

@@ -81,9 +81,9 @@ async def foo():
8181
n.start_soon(lambda n: n + 1)
8282

8383

84-
# body isn't a call to n.start
84+
# body is a call to await n.start
8585
async def foo_1():
86-
with trio.open_nursery(...) as n:
86+
with trio.open_nursery(...) as n: # error: 9, "n"
8787
await n.start(...)
8888

8989

tests/eval_files/async112_anyio.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# main tests in async112.py
2+
# this only tests anyio.create_task_group in particular
3+
# BASE_LIBRARY anyio
4+
# ASYNCIO_NO_ERROR
5+
# TRIO_NO_ERROR
6+
7+
import anyio
8+
9+
10+
async def bar(*args): ...
11+
12+
13+
async def foo():
14+
async with anyio.create_task_group() as tg: # error: 15, "tg"
15+
await tg.start_soon(bar())
16+
17+
async with anyio.create_task_group() as tg:
18+
await tg.start(bar(tg))
19+
20+
async with anyio.create_task_group() as tg: # error: 15, "tg"
21+
tg.start_soon(bar())
22+
23+
async with anyio.create_task_group() as tg:
24+
tg.start_soon(bar(tg))
25+
26+
# will not trigger on create_task
27+
async with anyio.create_task_group() as tg:
28+
tg.create_task(bar()) # type: ignore[attr-defined]

tests/eval_files/async112_asyncio.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# main tests in async112.py
2+
# this only tests asyncio.TaskGroup in particular
3+
# BASE_LIBRARY asyncio
4+
# ANYIO_NO_ERROR
5+
# TRIO_NO_ERROR
6+
# TaskGroup introduced in 3.11, we run typechecks with 3.9
7+
# mypy: disable-error-code=attr-defined
8+
9+
import asyncio
10+
11+
12+
async def bar(*args): ...
13+
14+
15+
async def foo():
16+
async with asyncio.TaskGroup() as tg: # error: 15, "tg"
17+
tg.create_task(bar())
18+
19+
async with asyncio.TaskGroup() as tg:
20+
tg.create_task(bar(tg))
21+
22+
# will not trigger on start / start_soon
23+
async with asyncio.TaskGroup() as tg:
24+
tg.start(bar())

0 commit comments

Comments
 (0)