Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add asyncio/anyio taskgroup support to async111&112, update contributing.md #241

Merged
merged 2 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@ You can instead of `error` specify the error code.
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.
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.

### `# NOANYIO` and `# NOTRIO`
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`.
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.
#### `# ANYIO_NO_ERROR`
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.
### Library parametrization
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`).
### `# BASE_LIBRARY anyio` / `# BASE_LIBRARY asyncio`
Defaults to `trio`. Used to specify the primary library an eval file is testing.

#### `# ANYIO_NO_ERROR`, `# TRIO_NO_ERROR`, `# ASYNCIO_NO_ERROR`
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.
### `# NOANYIO`, `# NOTRIO`, `#NOASYNCIO`
Disables checking a file with the specified library. Should be used somewhat sparingly, and always have a comment motivating its use.

## Running pytest outside tox
If you don't want to bother with tox to quickly test stuff, you'll need to install the following dependencies:
Expand Down
17 changes: 12 additions & 5 deletions flake8_async/visitors/visitor111.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,21 @@
from collections.abc import Mapping


def is_nursery_like(node: ast.expr) -> bool:
return bool(
get_matching_call(node, "open_nursery", base="trio")
or get_matching_call(node, "create_task_group", base="anyio")
or get_matching_call(node, "TaskGroup", base="asyncio")
)


@error_class
class Visitor111(Flake8AsyncVisitor):
error_codes: Mapping[str, str] = {
"ASYNC111": (
"variable {2} is usable within the context manager on line {0}, but that "
"will close before nursery opened on line {1} - this is usually a bug. "
"Nurseries should generally be the inner-most context manager."
"will close before nursery/taskgroup opened on line {1} - this is usually "
"a bug. Nursery/TaskGroup should generally be the inner-most context manager."
),
}

Expand Down Expand Up @@ -48,8 +56,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
self.TrioContextManager(
item.context_expr.lineno,
item.optional_vars.id,
get_matching_call(item.context_expr, "open_nursery")
is not None,
is_nursery_like(item.context_expr),
)
)

Expand All @@ -75,7 +82,7 @@ def visit_Call(self, node: ast.Call):
if (
isinstance(node.func, ast.Attribute)
and isinstance(node.func.value, ast.Name)
and node.func.attr in ("start", "start_soon")
and node.func.attr in ("start", "start_soon", "create_task")
):
self._nursery_call = None
for i, cm in enumerate(self._context_managers):
Expand Down
10 changes: 9 additions & 1 deletion tests/eval_files/async111.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# type: ignore
# 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.
# It's possible there's an equivalent asyncio construction/gotcha, but methods are differently named, so this file will not raise any errors
# ASYNCIO_NO_ERROR # no nurseries in asyncio. Though maybe the bug is relevant for TaskGroups
# nurseries are named taskgroups in asyncio/anyio
# ASYNCIO_NO_ERROR
# ANYIO_NO_ERROR
from typing import Any

import trio
Expand Down Expand Up @@ -242,3 +244,9 @@ def myfun(nursery, bar):
nursery.start(f)

# fmt: on

# visitor does not care to keep track of the type of nursery/taskgroup, so we
# raise errors on .create_task() even if it doesn't exist in trio.
with trio.open_nursery() as nursery:
with open("") as f:
nursery.create_task(f) # error: 28, line-1, line-2, "f", "create_task"
21 changes: 21 additions & 0 deletions tests/eval_files/async111_anyio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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"
23 changes: 23 additions & 0 deletions tests/eval_files/async111_asyncio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# main tests in async111.py
# this only tests asyncio.TaskGroup in particular
# BASE_LIBRARY asyncio
# ANYIO_NO_ERROR
# TRIO_NO_ERROR
# TaskGroup introduced in 3.11, we run typechecks with 3.9
# mypy: disable-error-code=attr-defined


import asyncio


async def bar(*args): ...


async def foo():
async with asyncio.TaskGroup() as tg:
with open("") as f:
tg.create_task(bar(f)) # error: 31, lineno-1, lineno-2, "f", "create_task"

# start[_soon] does not exist in asyncio, but gets errors anyway
await tg.start(bar, f) # error: 32, lineno-4, lineno-5, "f", "start"
tg.start_soon(bar, f) # error: 31, lineno-5, lineno-6, "f", "start_soon"
Loading