Skip to content

Commit f0ff928

Browse files
authored
Add config option for async900 decorators (#279)
1 parent bae7ab7 commit f0ff928

7 files changed

Lines changed: 47 additions & 10 deletions

File tree

docs/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ Changelog
44

55
*[CalVer, YY.month.patch](https://calver.org/)*
66

7+
24.8.1
8+
======
9+
- Add config option ``transform-async-generator-decorators``, to list decorators which
10+
suppress :ref:`ASYNC900 <async900>`.
11+
712
24.6.1
813
======
914
- Add :ref:`ASYNC120 <async120>` await-in-except.

docs/rules.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,15 @@ Optional rules disabled by default
161161
Our 9xx rules check for semantics issues, like 1xx rules, but are disabled by default due
162162
to the higher volume of warnings. We encourage you to enable them - without guaranteed
163163
:ref:`checkpoint`\ s timeouts and cancellation can be arbitrarily delayed, and async
164-
generators are prone to the problems described in :pep:`533`.
164+
generators are prone to the problems described in :pep:`789` and :pep:`533`.
165165

166166
_`ASYNC900` : unsafe-async-generator
167167
Async generator without :func:`@asynccontextmanager <contextlib.asynccontextmanager>` not allowed.
168168
You might want to enable this on a codebase since async generators are inherently unsafe and cleanup logic might not be performed.
169-
See `#211 <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.
169+
See :pep:`789` for control-flow problems, :pep:`533` for delayed cleanup problems.
170+
Further decorators can be registered with the ``--transform-async-generator-decorators``
171+
config option, e.g. `@trio_util.trio_async_generator
172+
<https://trio-util.readthedocs.io/en/latest/index.html#trio_util.trio_async_generator>`_.
170173

171174
_`ASYNC910` : async-function-no-checkpoint
172175
Exit or ``return`` from async function with no guaranteed :ref:`checkpoint` or exception since function definition.

flake8_async/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939

4040
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41-
__version__ = "24.6.1"
41+
__version__ = "24.8.1"
4242

4343

4444
# taken from https://github.com/Zac-HD/shed
@@ -261,6 +261,18 @@ def add_options(option_manager: OptionManager | ArgumentParser):
261261
"mydecorator,mypackage.mydecorators.*``"
262262
),
263263
)
264+
add_argument(
265+
"--transform-async-generator-decorators",
266+
default="",
267+
required=False,
268+
type=comma_separated_list,
269+
help=(
270+
"Comma-separated list of decorators to disable ASYNC900 warnings for. "
271+
"Decorators can be dotted or not, as well as support * as a wildcard. "
272+
"For example, ``--transform-async-generator-decorators=fastapi.Depends,"
273+
"trio_util.trio_async_generator``"
274+
),
275+
)
264276
add_argument(
265277
"--exception-suppress-context-managers",
266278
default="",
@@ -391,6 +403,7 @@ def get_matching_codes(
391403
autofix_codes=autofix_codes,
392404
error_on_autofix=options.error_on_autofix,
393405
no_checkpoint_warning_decorators=options.no_checkpoint_warning_decorators,
406+
transform_async_generator_decorators=options.transform_async_generator_decorators,
394407
exception_suppress_context_managers=options.exception_suppress_context_managers,
395408
startable_in_context_manager=options.startable_in_context_manager,
396409
async200_blocking_calls=options.async200_blocking_calls,

flake8_async/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class Options:
2929
# whether to print an error message even when autofixed
3030
error_on_autofix: bool
3131
no_checkpoint_warning_decorators: Collection[str]
32+
transform_async_generator_decorators: Collection[str]
3233
exception_suppress_context_managers: Collection[str]
3334
startable_in_context_manager: Collection[str]
3435
async200_blocking_calls: dict[str, str]

flake8_async/visitors/visitors.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,27 +401,33 @@ def leave_IfExp_test(self, node: cst.IfExp):
401401
@disabled_by_default
402402
class Visitor900(Flake8AsyncVisitor):
403403
error_codes: Mapping[str, str] = {
404-
"ASYNC900": "Async generator without `@asynccontextmanager` not allowed."
404+
"ASYNC900": "Async generator not allowed, unless transformed "
405+
"by a known decorator (one of: {})."
405406
}
406407

407408
def __init__(self, *args: Any, **kwargs: Any):
408409
super().__init__(*args, **kwargs)
409410
self.unsafe_function: ast.AsyncFunctionDef | None = None
411+
self.transform_decorators = (
412+
"asynccontextmanager",
413+
"fixture",
414+
*self.options.transform_async_generator_decorators,
415+
)
410416

411417
def visit_AsyncFunctionDef(
412418
self, node: ast.AsyncFunctionDef | ast.FunctionDef | ast.Lambda
413419
):
414420
self.save_state(node, "unsafe_function")
415421
if isinstance(node, ast.AsyncFunctionDef) and not has_decorator(
416-
node, "asynccontextmanager", "fixture"
422+
node, *self.transform_decorators
417423
):
418424
self.unsafe_function = node
419425
else:
420426
self.unsafe_function = None
421427

422428
def visit_Yield(self, node: ast.Yield):
423429
if self.unsafe_function is not None:
424-
self.error(self.unsafe_function)
430+
self.error(self.unsafe_function, ", ".join(self.transform_decorators))
425431
self.unsafe_function = None
426432

427433
visit_FunctionDef = visit_AsyncFunctionDef

setup.py

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

44
from pathlib import Path
55

6-
from setuptools import find_packages, setup
6+
from setuptools import find_packages, setup # type: ignore
77

88

99
def local_file(name: str) -> Path:

tests/eval_files/async900.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from contextlib import asynccontextmanager
44

55

6-
async def foo1(): # ASYNC900: 0
6+
async def foo1(): # ASYNC900: 0, 'asynccontextmanager, fixture, this_is_like_a_context_manager'
77
yield
88
yield
99

@@ -15,7 +15,7 @@ async def foo2():
1515

1616
@asynccontextmanager
1717
async def foo3():
18-
async def bar(): # ASYNC900: 4
18+
async def bar(): # ASYNC900: 4, 'asynccontextmanager, fixture, this_is_like_a_context_manager'
1919
yield
2020

2121
yield
@@ -37,7 +37,7 @@ async def async_fixtures_can_take_arguments():
3737

3838
# no-checkpoint-warning-decorator now ignored
3939
@other_context_manager
40-
async def foo5(): # ASYNC900: 0
40+
async def foo5(): # ASYNC900: 0, 'asynccontextmanager, fixture, this_is_like_a_context_manager'
4141
yield
4242

4343

@@ -54,3 +54,12 @@ async def cm():
5454
async def another_non_generator():
5555
def foo():
5656
yield
57+
58+
59+
# ARG --transform-async-generator-decorators=this_is_like_a_context_manager
60+
61+
62+
@this_is_like_a_context_manager() # OK because of the config, issue #277
63+
async def some_generator():
64+
while True:
65+
yield

0 commit comments

Comments
 (0)