From ca624190f6fd5512925b60efb9a8a2f3679919fe Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Wed, 7 Aug 2024 00:51:34 -0700 Subject: [PATCH] Add config option for async900 decorators --- docs/changelog.rst | 5 +++++ docs/rules.rst | 7 +++++-- flake8_async/__init__.py | 15 ++++++++++++++- flake8_async/base.py | 1 + flake8_async/visitors/visitors.py | 12 +++++++++--- setup.py | 2 +- tests/eval_files/async900.py | 15 ++++++++++++--- 7 files changed, 47 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index f70171ee..daefaa6e 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,11 @@ Changelog *[CalVer, YY.month.patch](https://calver.org/)* +24.8.1 +====== +- Add config option ``transform-async-generator-decorators``, to list decorators which + suppress :ref:`ASYNC900 `. + 24.6.1 ====== - Add :ref:`ASYNC120 ` await-in-except. diff --git a/docs/rules.rst b/docs/rules.rst index 6c0738c4..3eede308 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -161,12 +161,15 @@ Optional rules disabled by default Our 9xx rules check for semantics issues, like 1xx rules, but are disabled by default due to the higher volume of warnings. We encourage you to enable them - without guaranteed :ref:`checkpoint`\ s timeouts and cancellation can be arbitrarily delayed, and async -generators are prone to the problems described in :pep:`533`. +generators are prone to the problems described in :pep:`789` and :pep:`533`. _`ASYNC900` : unsafe-async-generator Async generator without :func:`@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 `#211 `__ and https://discuss.python.org/t/using-exceptiongroup-at-anthropic-experience-report/20888/6 for discussion. + See :pep:`789` for control-flow problems, :pep:`533` for delayed cleanup problems. + Further decorators can be registered with the ``--transform-async-generator-decorators`` + config option, e.g. `@trio_util.trio_async_generator + `_. _`ASYNC910` : async-function-no-checkpoint Exit or ``return`` from async function with no guaranteed :ref:`checkpoint` or exception since function definition. diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index b85a8283..9e229e6f 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__ = "24.6.1" +__version__ = "24.8.1" # taken from https://github.com/Zac-HD/shed @@ -261,6 +261,18 @@ def add_options(option_manager: OptionManager | ArgumentParser): "mydecorator,mypackage.mydecorators.*``" ), ) + add_argument( + "--transform-async-generator-decorators", + default="", + required=False, + type=comma_separated_list, + help=( + "Comma-separated list of decorators to disable ASYNC900 warnings for. " + "Decorators can be dotted or not, as well as support * as a wildcard. " + "For example, ``--transform-async-generator-decorators=fastapi.Depends," + "trio_util.trio_async_generator``" + ), + ) add_argument( "--exception-suppress-context-managers", default="", @@ -391,6 +403,7 @@ def get_matching_codes( autofix_codes=autofix_codes, error_on_autofix=options.error_on_autofix, no_checkpoint_warning_decorators=options.no_checkpoint_warning_decorators, + transform_async_generator_decorators=options.transform_async_generator_decorators, exception_suppress_context_managers=options.exception_suppress_context_managers, startable_in_context_manager=options.startable_in_context_manager, async200_blocking_calls=options.async200_blocking_calls, diff --git a/flake8_async/base.py b/flake8_async/base.py index 866df2d4..173bdfb3 100644 --- a/flake8_async/base.py +++ b/flake8_async/base.py @@ -29,6 +29,7 @@ class Options: # whether to print an error message even when autofixed error_on_autofix: bool no_checkpoint_warning_decorators: Collection[str] + transform_async_generator_decorators: Collection[str] exception_suppress_context_managers: Collection[str] startable_in_context_manager: Collection[str] async200_blocking_calls: dict[str, str] diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 1ea540ab..d6b1363b 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -401,19 +401,25 @@ def leave_IfExp_test(self, node: cst.IfExp): @disabled_by_default class Visitor900(Flake8AsyncVisitor): error_codes: Mapping[str, str] = { - "ASYNC900": "Async generator without `@asynccontextmanager` not allowed." + "ASYNC900": "Async generator not allowed, unless transformed " + "by a known decorator (one of: {})." } def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.unsafe_function: ast.AsyncFunctionDef | None = None + self.transform_decorators = ( + "asynccontextmanager", + "fixture", + *self.options.transform_async_generator_decorators, + ) def visit_AsyncFunctionDef( self, node: ast.AsyncFunctionDef | ast.FunctionDef | ast.Lambda ): self.save_state(node, "unsafe_function") if isinstance(node, ast.AsyncFunctionDef) and not has_decorator( - node, "asynccontextmanager", "fixture" + node, *self.transform_decorators ): self.unsafe_function = node else: @@ -421,7 +427,7 @@ def visit_AsyncFunctionDef( def visit_Yield(self, node: ast.Yield): if self.unsafe_function is not None: - self.error(self.unsafe_function) + self.error(self.unsafe_function, ", ".join(self.transform_decorators)) self.unsafe_function = None visit_FunctionDef = visit_AsyncFunctionDef diff --git a/setup.py b/setup.py index c397b4df..0f19b457 100755 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from pathlib import Path -from setuptools import find_packages, setup +from setuptools import find_packages, setup # type: ignore def local_file(name: str) -> Path: diff --git a/tests/eval_files/async900.py b/tests/eval_files/async900.py index 235d211b..3bf43037 100644 --- a/tests/eval_files/async900.py +++ b/tests/eval_files/async900.py @@ -3,7 +3,7 @@ from contextlib import asynccontextmanager -async def foo1(): # ASYNC900: 0 +async def foo1(): # ASYNC900: 0, 'asynccontextmanager, fixture, this_is_like_a_context_manager' yield yield @@ -15,7 +15,7 @@ async def foo2(): @asynccontextmanager async def foo3(): - async def bar(): # ASYNC900: 4 + async def bar(): # ASYNC900: 4, 'asynccontextmanager, fixture, this_is_like_a_context_manager' yield yield @@ -37,7 +37,7 @@ async def async_fixtures_can_take_arguments(): # no-checkpoint-warning-decorator now ignored @other_context_manager -async def foo5(): # ASYNC900: 0 +async def foo5(): # ASYNC900: 0, 'asynccontextmanager, fixture, this_is_like_a_context_manager' yield @@ -54,3 +54,12 @@ async def cm(): async def another_non_generator(): def foo(): yield + + +# ARG --transform-async-generator-decorators=this_is_like_a_context_manager + + +@this_is_like_a_context_manager() # OK because of the config, issue #277 +async def some_generator(): + while True: + yield