diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 48e58f8b..536433b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', 3.13-dev] + python-version: ['3.9', '3.10', '3.11', '3.12', 3.13] fail-fast: false steps: - uses: actions/checkout@v4 diff --git a/docs/changelog.rst b/docs/changelog.rst index 2672b087..12bbe51b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -6,6 +6,7 @@ Changelog 24.10.2 ======= +- :ref:`ASYNC101 ` and :ref:`ASYNC119 ` are now silenced for decorators in :ref:`transform-async-generator-decorators` - :ref:`ASYNC102 ` now also warns about ``await()`` inside ``__aexit__``. 24.10.1 @@ -39,7 +40,7 @@ Changelog 24.8.1 ====== -- Add config option ``transform-async-generator-decorators``, to list decorators which +- Add config option :ref:`transform-async-generator-decorators`, to list decorators which suppress :ref:`ASYNC900 `. 24.6.1 diff --git a/docs/rules.rst b/docs/rules.rst index c3b7566d..41668c1d 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -15,7 +15,7 @@ _`ASYNC100` : cancel-scope-no-checkpoint This check also treats ``yield`` as a checkpoint, since checkpoints can happen in the caller we yield to. See :ref:`ASYNC912 ` which will in addition guarantee checkpoints on every code path. -ASYNC101 : yield-in-cancel-scope +_`ASYNC101` : yield-in-cancel-scope ``yield`` inside a :ref:`taskgroup_nursery` or :ref:`timeout_context` is only safe when implementing a context manager - otherwise, it breaks exception handling. See `this thread `_ for discussion of a future PEP. This has substantial overlap with :ref:`ASYNC119 `, which will warn on almost all instances of ASYNC101, but ASYNC101 is about a conceptually different problem that will not get resolved by `PEP 533 `_. diff --git a/docs/usage.rst b/docs/usage.rst index 3a33e7a3..4ec19e14 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -318,3 +318,21 @@ Specified patterns must not have parentheses, and will only match when the patte def my_blocking_call(): # it's also safe to use the name in other contexts ... arbitrary_other_function(my_blocking_call=None) + +.. _transform-async-generator-decorators: + +``transform-async-generator-decorators`` +---------------------------------------- +Comma-separated list of decorators that make async generators safe, disabling +:ref:`ASYNC900 `, :ref:`ASYNC101 `, and :ref:`ASYNC119 ` warnings for functions decorated with any of them. +``[pytest.]fixture`` and ``[contextlib.]asynccontextmanager`` are always considered safe. +Decorators can be dotted or not, as well as support * as a wildcard. + +Example +^^^^^^^ + +.. code-block:: none + + transform-async-generator-decorators = + fastapi.Depends + trio_util.trio_async_generator diff --git a/flake8_async/visitors/visitor101.py b/flake8_async/visitors/visitor101.py index a5edcd93..59d04638 100644 --- a/flake8_async/visitors/visitor101.py +++ b/flake8_async/visitors/visitor101.py @@ -70,7 +70,11 @@ def visit_FunctionDef(self, node: cst.FunctionDef): self.save_state(node, "_yield_is_error", "_safe_decorator") self._yield_is_error = False self._safe_decorator = func_has_decorator( - node, "contextmanager", "asynccontextmanager", "fixture" + node, + "contextmanager", + "asynccontextmanager", + "fixture", + *self.options.transform_async_generator_decorators, ) # trigger on leaving yield so any comments are parsed for noqas diff --git a/flake8_async/visitors/visitors.py b/flake8_async/visitors/visitors.py index 6551f9ec..bebd01d8 100644 --- a/flake8_async/visitors/visitors.py +++ b/flake8_async/visitors/visitors.py @@ -334,7 +334,10 @@ def visit_AsyncFunctionDef( self.save_state(node, "unsafe_function", "contextmanager") self.contextmanager = False if isinstance(node, ast.AsyncFunctionDef) and not has_decorator( - node, "asynccontextmanager" + node, + "asynccontextmanager", + "fixture", + *self.options.transform_async_generator_decorators, ): self.unsafe_function = True else: diff --git a/tests/eval_files/async101.py b/tests/eval_files/async101.py index 88e3b344..1623f244 100644 --- a/tests/eval_files/async101.py +++ b/tests/eval_files/async101.py @@ -1,4 +1,6 @@ # ASYNCIO_NO_ERROR +# ARG --no-checkpoint-warning-decorator=no_checkpoint_warning_decorator +# ARG --transform-async-generator-decorators=transform_async_gen_decorator # This file contains errors shared between trio and anyio, since they have some # overlap in naming. @@ -127,3 +129,23 @@ def foo_pytest_fixture_paren(): def foo_pytest_fixture_params(): with trio.CancelScope() as _: yield 1 + + +def no_checkpoint_warning_decorator(_: object): ... + + +def transform_async_gen_decorator(_: object): ... + + +# --no-checkpoint-warning-decorator does not mark as safe +@no_checkpoint_warning_decorator +def no_checkpoint_warning_deco_fun(): + with trio.CancelScope(): + yield 1 # error: 8 + + +# --transform-async-generator-decorators marks as safe +@transform_async_gen_decorator +def transfor_async_gen_deco_fun(): + with trio.CancelScope(): + yield 1 # safe diff --git a/tests/eval_files/async119.py b/tests/eval_files/async119.py index 242db2c5..63f2ca66 100644 --- a/tests/eval_files/async119.py +++ b/tests/eval_files/async119.py @@ -1,3 +1,5 @@ +# ARG --no-checkpoint-warning-decorator=no_checkpoint_warning_decorator +# ARG --transform-async-generator-decorators=transform_async_gen_decorator import contextlib from contextlib import asynccontextmanager @@ -62,3 +64,21 @@ async def safe_in_contextmanager(): async def safe_in_contextmanager2(): with open(""): yield + + +def no_checkpoint_warning_decorator(_: object): ... + + +def transform_async_gen_decorator(_: object): ... + + +@no_checkpoint_warning_decorator +async def no_checkpoint_warning_deco_fun(): + with open(""): + yield # error: 8 + + +@transform_async_gen_decorator +async def transfor_async_gen_deco_fun(): + with open(""): + yield # safe diff --git a/tests/eval_files/async900.py b/tests/eval_files/async900.py index 3bf43037..c4df99b4 100644 --- a/tests/eval_files/async900.py +++ b/tests/eval_files/async900.py @@ -1,5 +1,6 @@ # type: ignore # ARG --no-checkpoint-warning-decorator=asynccontextmanager,other_context_manager +# transform-async-generator-decorators set further down from contextlib import asynccontextmanager