Skip to content

Commit 32167c3

Browse files
authored
add async119: yield in contextmanager in async generator (#238)
* add async119: yield in contextmanager in async generator * fix version * make async900 label point directly to the async900 rule. Save bools instead of nodes since we don't need to warn on them or refer to them. Warn on each yield, add test case for that.
1 parent d1c54fb commit 32167c3

File tree

6 files changed

+113
-1
lines changed

6 files changed

+113
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Changelog
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

4+
## 24.4.2
5+
- Add ASYNC119: yield in contextmanager in async generator.
46

57
## 24.4.1
68
- ASYNC91X fix internal error caused by multiple `try/except` incorrectly sharing state.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pip install flake8-async
4141
- **ASYNC115**: Replace `[trio/anyio].sleep(0)` with the more suggestive `[trio/anyio].lowlevel.checkpoint()`.
4242
- **ASYNC116**: `[trio/anyio].sleep()` with >24 hour interval should usually be `[trio/anyio].sleep_forever()`.
4343
- **ASYNC118**: Don't assign the value of `anyio.get_cancelled_exc_class()` to a variable, since that breaks linter checks and multi-backend programs.
44+
- **ASYNC119**: `yield` in context manager in async generator is unsafe, the cleanup may be delayed until `await` is no longer allowed. We strongly encourage you to read PEP-533 and use `async with aclosing(...)`, or better yet avoid async generators entirely (see ASYNC900) in favor of context managers which return an iterable channel/queue.
4445

4546
### Warnings for blocking sync calls in async functions
4647
Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.

docs/rules.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ General rules
2121
- **ASYNC115**: Replace ``[trio/anyio].sleep(0)`` with the more suggestive ``[trio/anyio].lowlevel.checkpoint()``.
2222
- **ASYNC116**: ``[trio/anyio].sleep()`` with >24 hour interval should usually be ``[trio/anyio].sleep_forever()``.
2323
- **ASYNC118**: Don't assign the value of ``anyio.get_cancelled_exc_class()`` to a variable, since that breaks linter checks and multi-backend programs.
24+
- **ASYNC119**: ``yield`` in context manager in async generator is unsafe, the cleanup may be delayed until ``await`` is no longer allowed. We strongly encourage you to read `PEP 533 <https://peps.python.org/pep-0533/>`_ and use `async with aclosing(...) <https://docs.python.org/3/library/contextlib.html#contextlib.aclosing>`_, or better yet avoid async generators entirely (see :ref:`ASYNC900 <async900>` ) in favor of context managers which return an iterable `channel (trio) <https://trio.readthedocs.io/en/stable/reference-core.html#channels>`_, `stream (anyio) <https://anyio.readthedocs.io/en/stable/streams.html#streams>`_, or `queue (asyncio) <https://docs.python.org/3/library/asyncio-queue.html>`_.
25+
26+
.. TODO: use intersphinx(?) instead of having to specify full URL
2427
2528
Blocking sync calls in async functions
2629
======================================
@@ -42,9 +45,12 @@ Note: 22X, 23X and 24X has not had asyncio-specific suggestions written.
4245
- **ASYNC250**: Builtin ``input()`` should not be called from async function. Wrap in ``[trio/anyio].to_thread.run_sync()`` or ``asyncio.loop.run_in_executor()``.
4346
- **ASYNC251**: ``time.sleep(...)`` should not be called from async function. Use ``[trio/anyio/asyncio].sleep(...)``.
4447

48+
4549
Optional rules disabled by default
4650
==================================
4751

52+
.. _async900:
53+
4854
- **ASYNC900**: Async generator without ``@asynccontextmanager`` not allowed. You might want to enable this on a codebase since async generators are inherently unsafe and cleanup logic might not be performed. See https://github.com/python-trio/flake8-async/issues/211 and https://discuss.python.org/t/using-exceptiongroup-at-anthropic-experience-report/20888/6 for discussion.
4955
- **ASYNC910**: Exit or ``return`` from async function with no guaranteed checkpoint or exception since function definition. You might want to enable this on a codebase to make it easier to reason about checkpoints, and make the logic of ASYNC911 correct.
5056
- **ASYNC911**: Exit, ``yield`` or ``return`` from async iterable with no guaranteed checkpoint since possible function entry (yield or function definition)

flake8_async/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
40-
__version__ = "24.4.1"
40+
__version__ = "24.4.2"
4141

4242

4343
# taken from https://github.com/Zac-HD/shed

flake8_async/visitors/visitors.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,45 @@ def visit_Call(self, node: ast.Call):
281281
self.error(node, m[2])
282282

283283

284+
@error_class
285+
class Visitor119(Flake8AsyncVisitor):
286+
error_codes: Mapping[str, str] = {
287+
"ASYNC119": "Yield in contextmanager in async generator might not trigger"
288+
" cleanup. Use `@asynccontextmanager` or refactor."
289+
}
290+
291+
def __init__(self, *args: Any, **kwargs: Any):
292+
super().__init__(*args, **kwargs)
293+
self.unsafe_function: bool = False
294+
self.contextmanager: bool = False
295+
296+
def visit_AsyncFunctionDef(
297+
self, node: ast.AsyncFunctionDef | ast.FunctionDef | ast.Lambda
298+
):
299+
self.save_state(node, "unsafe_function", "contextmanager")
300+
self.contextmanager = False
301+
if isinstance(node, ast.AsyncFunctionDef) and not has_decorator(
302+
node, "asynccontextmanager"
303+
):
304+
self.unsafe_function = True
305+
else:
306+
self.unsafe_function = False
307+
308+
def visit_With(self, node: ast.With | ast.AsyncWith):
309+
self.save_state(node, "contextmanager")
310+
self.contextmanager = True
311+
312+
def visit_Yield(self, node: ast.Yield):
313+
if self.unsafe_function and self.contextmanager:
314+
self.error(node)
315+
316+
visit_AsyncWith = visit_With
317+
visit_FunctionDef = visit_AsyncFunctionDef
318+
# it's not possible to yield or open context managers in lambda's, so this
319+
# one isn't strictly needed afaik.
320+
visit_Lambda = visit_AsyncFunctionDef
321+
322+
284323
@error_class
285324
@disabled_by_default
286325
class Visitor900(Flake8AsyncVisitor):

tests/eval_files/async119.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import contextlib
2+
3+
from contextlib import asynccontextmanager
4+
5+
6+
async def unsafe_yield():
7+
with open(""):
8+
yield # error: 8
9+
10+
11+
async def async_with():
12+
async with unsafe_yield():
13+
yield # error: 8
14+
15+
16+
async def warn_on_yeach_yield():
17+
with open(""):
18+
yield # error: 8
19+
yield # error: 8
20+
with open(""):
21+
yield # error: 8
22+
yield # error: 8
23+
24+
25+
async def yield_not_in_contextmanager():
26+
yield
27+
with open(""):
28+
...
29+
yield
30+
31+
32+
async def yield_in_nested_function():
33+
with open(""):
34+
35+
def foo():
36+
yield
37+
38+
39+
async def yield_in_nested_async_function():
40+
with open(""):
41+
42+
async def foo():
43+
yield
44+
45+
46+
async def yield_after_nested_async_function():
47+
with open(""):
48+
49+
async def foo():
50+
yield
51+
52+
yield # error: 8
53+
54+
55+
@asynccontextmanager
56+
async def safe_in_contextmanager():
57+
with open(""):
58+
yield
59+
60+
61+
@contextlib.asynccontextmanager
62+
async def safe_in_contextmanager2():
63+
with open(""):
64+
yield

0 commit comments

Comments
 (0)