From b15d6323b05d2ddda396666d876828d39aadf2e2 Mon Sep 17 00:00:00 2001 From: Roman Valov Date: Thu, 24 Jul 2025 20:56:25 +0000 Subject: [PATCH 1/3] async102 not applicable to asyncio --- docs/changelog.rst | 4 ++++ docs/rules.rst | 2 +- docs/usage.rst | 2 +- flake8_async/__init__.py | 2 +- flake8_async/visitors/visitor102_120.py | 8 +++----- tests/eval_files/async102.py | 2 +- tests/eval_files/async102_aclose.py | 2 ++ tests/eval_files/async102_aclose_args.py | 2 ++ tests/eval_files/async102_anyio.py | 2 +- tests/eval_files/async102_asyncio.py | 1 + tests/eval_files/async102_trio.py | 2 +- tests/eval_files/noqa_no_autofix.py | 1 + 12 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 26b10c57..1064da0a 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,10 @@ Changelog `CalVer, YY.month.patch `_ +25.7.1 +====== +- :ref:`ASYNC102 ` no longer triggered for asyncio due to different cancellation semantics it uses. + 25.5.3 ====== - :ref:`ASYNC115 ` and :ref:`ASYNC116 ` now also checks kwargs. diff --git a/docs/rules.rst b/docs/rules.rst index 4a640aa4..afc7943e 100644 --- a/docs/rules.rst +++ b/docs/rules.rst @@ -24,8 +24,8 @@ _`ASYNC101` : yield-in-cancel-scope _`ASYNC102` : await-in-finally-or-cancelled ``await`` inside ``finally``, :ref:`cancelled-catching ` ``except:``, or ``__aexit__`` must have shielded :ref:`cancel scope ` with timeout. If not, the async call will immediately raise a new cancellation, suppressing any cancellation that was caught. + Not applicable to asyncio due to edge-based cancellation semantics it uses as opposed to level-based used by trio and anyio. See :ref:`ASYNC120 ` for the general case where other exceptions might get suppressed. - This is currently not able to detect asyncio shields. ASYNC103 : no-reraise-cancelled :ref:`cancelled`-catching exception that does not reraise the exception. diff --git a/docs/usage.rst b/docs/usage.rst index b1bb6c23..46827999 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -33,7 +33,7 @@ adding the following to your ``.pre-commit-config.yaml``: minimum_pre_commit_version: '2.9.0' repos: - repo: https://github.com/python-trio/flake8-async - rev: 25.5.3 + rev: 25.7.1 hooks: - id: flake8-async # args: ["--enable=ASYNC100,ASYNC112", "--disable=", "--autofix=ASYNC"] diff --git a/flake8_async/__init__.py b/flake8_async/__init__.py index c0ba7b3a..89eb93dd 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__ = "25.5.3" +__version__ = "25.7.1" # taken from https://github.com/Zac-HD/shed diff --git a/flake8_async/visitors/visitor102_120.py b/flake8_async/visitors/visitor102_120.py index 1187e270..1a1dc770 100644 --- a/flake8_async/visitors/visitor102_120.py +++ b/flake8_async/visitors/visitor102_120.py @@ -74,7 +74,8 @@ def async_call_checker( # non-critical exception handlers have the statement name set to "except" if self._critical_scope.name == "except": self._potential_120.append((node, self._critical_scope)) - else: + # not applicable to asyncio due to different cancellation semantics it uses + elif self.library != ("asyncio",): self.error(node, self._critical_scope, error_code="ASYNC102") def visit_Raise(self, node: ast.Raise): @@ -84,10 +85,7 @@ def visit_Raise(self, node: ast.Raise): def is_safe_aclose_call(self, node: ast.Await) -> bool: return ( - # don't mark calls safe in asyncio-only files - # a more defensive option would be `asyncio not in self.library` - self.library != ("asyncio",) - and isinstance(node.value, ast.Call) + isinstance(node.value, ast.Call) # only known safe if no arguments and not node.value.args and not node.value.keywords diff --git a/tests/eval_files/async102.py b/tests/eval_files/async102.py index 3d3f6d64..622ab10c 100644 --- a/tests/eval_files/async102.py +++ b/tests/eval_files/async102.py @@ -1,6 +1,6 @@ # type: ignore # ARG --enable=ASYNC102,ASYNC120 -# NOASYNCIO # TODO: support asyncio shields +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio from contextlib import asynccontextmanager import trio diff --git a/tests/eval_files/async102_aclose.py b/tests/eval_files/async102_aclose.py index 4cba907f..683f81a3 100644 --- a/tests/eval_files/async102_aclose.py +++ b/tests/eval_files/async102_aclose.py @@ -7,6 +7,8 @@ # ANYIO_NO_ERROR # TRIO_NO_ERROR +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio + # See also async102_aclose_args.py - which makes sure trio/anyio raises errors if there # are arguments to aclose(). diff --git a/tests/eval_files/async102_aclose_args.py b/tests/eval_files/async102_aclose_args.py index 6db0d9fe..a534360f 100644 --- a/tests/eval_files/async102_aclose_args.py +++ b/tests/eval_files/async102_aclose_args.py @@ -1,5 +1,7 @@ # type: ignore +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio + # trio/anyio should still raise errors if there's args # asyncio will always raise errors diff --git a/tests/eval_files/async102_anyio.py b/tests/eval_files/async102_anyio.py index f31eb114..61d1ea54 100644 --- a/tests/eval_files/async102_anyio.py +++ b/tests/eval_files/async102_anyio.py @@ -1,6 +1,6 @@ # type: ignore # NOTRIO -# NOASYNCIO +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio # BASE_LIBRARY anyio # this test will raise the same errors with trio/asyncio, despite [trio|asyncio].get_cancelled_exc_class not existing # marked not to run the tests though as error messages will only refer to anyio diff --git a/tests/eval_files/async102_asyncio.py b/tests/eval_files/async102_asyncio.py index 5dabb985..813725d6 100644 --- a/tests/eval_files/async102_asyncio.py +++ b/tests/eval_files/async102_asyncio.py @@ -2,6 +2,7 @@ # NOANYIO # NOTRIO # BASE_LIBRARY asyncio +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio from contextlib import asynccontextmanager import asyncio diff --git a/tests/eval_files/async102_trio.py b/tests/eval_files/async102_trio.py index 7f1a5b93..d956c91a 100644 --- a/tests/eval_files/async102_trio.py +++ b/tests/eval_files/async102_trio.py @@ -1,5 +1,5 @@ -# NOASYNCIO # NOANYIO - since anyio.Cancelled does not exist +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio import trio diff --git a/tests/eval_files/noqa_no_autofix.py b/tests/eval_files/noqa_no_autofix.py index 36f98bf9..8897e7ce 100644 --- a/tests/eval_files/noqa_no_autofix.py +++ b/tests/eval_files/noqa_no_autofix.py @@ -1,4 +1,5 @@ # ARG --enable=ASYNC102 +# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio import trio from typing import Any From 4146bb433316675b06915f94d0dc6fc513a2c048 Mon Sep 17 00:00:00 2001 From: Roman Valov Date: Mon, 28 Jul 2025 23:03:03 +0000 Subject: [PATCH 2/3] update tests to reflect async102 changes --- tests/eval_files/async102.py | 36 +++++++++++++++++ tests/eval_files/async102_aclose.py | 28 -------------- tests/eval_files/async102_aclose_args.py | 26 ------------- tests/eval_files/async102_anyio.py | 4 +- tests/eval_files/async102_asyncio.py | 49 ------------------------ tests/eval_files/noqa_no_autofix.py | 24 ++++-------- 6 files changed, 45 insertions(+), 122 deletions(-) delete mode 100644 tests/eval_files/async102_aclose.py delete mode 100644 tests/eval_files/async102_aclose_args.py delete mode 100644 tests/eval_files/async102_asyncio.py diff --git a/tests/eval_files/async102.py b/tests/eval_files/async102.py index 622ab10c..bfc25a71 100644 --- a/tests/eval_files/async102.py +++ b/tests/eval_files/async102.py @@ -310,3 +310,39 @@ async def foo_nested_cs(): # treat __aexit__ as a critical scope async def __aexit__(): await foo() # error: 4, Statement("__aexit__", lineno-1) + + +# exclude finally: await x.aclose() +# trio/anyio marks arg-less aclose() as safe +async def foo_aclose_noargs(): + # no type tracking in this check, we allow any call that looks like + # `await [...].aclose()` + x = None + + try: + ... + except BaseException: + await x.aclose() + await x.y.aclose() + finally: + await x.aclose() + await x.y.aclose() + + +# trio/anyio should still raise errors if there's args +async def foo(): + # no type tracking in this check + x = None + + try: + ... + except BaseException: + await x.aclose(foo) # ASYNC102: 8, Statement("BaseException", lineno-1) + await x.aclose(bar=foo) # ASYNC102: 8, Statement("BaseException", lineno-2) + await x.aclose(*foo) # ASYNC102: 8, Statement("BaseException", lineno-3) + await x.aclose(None) # ASYNC102: 8, Statement("BaseException", lineno-4) + finally: + await x.aclose(foo) # ASYNC102: 8, Statement("try/finally", lineno-8) + await x.aclose(bar=foo) # ASYNC102: 8, Statement("try/finally", lineno-9) + await x.aclose(*foo) # ASYNC102: 8, Statement("try/finally", lineno-10) + await x.aclose(None) # ASYNC102: 8, Statement("try/finally", lineno-11) diff --git a/tests/eval_files/async102_aclose.py b/tests/eval_files/async102_aclose.py deleted file mode 100644 index 683f81a3..00000000 --- a/tests/eval_files/async102_aclose.py +++ /dev/null @@ -1,28 +0,0 @@ -# type: ignore -# ARG --enable=ASYNC102,ASYNC120 - -# exclude finally: await x.aclose() from async102 and async120, with trio/anyio - -# These magical markers are the ones that ensure trio & anyio don't raise errors: -# ANYIO_NO_ERROR -# TRIO_NO_ERROR - -# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio - -# See also async102_aclose_args.py - which makes sure trio/anyio raises errors if there -# are arguments to aclose(). - - -async def foo(): - # no type tracking in this check, we allow any call that looks like - # `await [...].aclose()` - x = None - - try: - ... - except BaseException: - await x.aclose() # ASYNC102: 8, Statement("BaseException", lineno-1) - await x.y.aclose() # ASYNC102: 8, Statement("BaseException", lineno-2) - finally: - await x.aclose() # ASYNC102: 8, Statement("try/finally", lineno-6) - await x.y.aclose() # ASYNC102: 8, Statement("try/finally", lineno-7) diff --git a/tests/eval_files/async102_aclose_args.py b/tests/eval_files/async102_aclose_args.py deleted file mode 100644 index a534360f..00000000 --- a/tests/eval_files/async102_aclose_args.py +++ /dev/null @@ -1,26 +0,0 @@ -# type: ignore - -# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio - -# trio/anyio should still raise errors if there's args -# asyncio will always raise errors - -# See also async102_aclose.py, which checks that trio/anyio marks arg-less aclose() as safe - - -async def foo(): - # no type tracking in this check - x = None - - try: - ... - except BaseException: - await x.aclose(foo) # ASYNC102: 8, Statement("BaseException", lineno-1) - await x.aclose(bar=foo) # ASYNC102: 8, Statement("BaseException", lineno-2) - await x.aclose(*foo) # ASYNC102: 8, Statement("BaseException", lineno-3) - await x.aclose(None) # ASYNC102: 8, Statement("BaseException", lineno-4) - finally: - await x.aclose(foo) # ASYNC102: 8, Statement("try/finally", lineno-8) - await x.aclose(bar=foo) # ASYNC102: 8, Statement("try/finally", lineno-9) - await x.aclose(*foo) # ASYNC102: 8, Statement("try/finally", lineno-10) - await x.aclose(None) # ASYNC102: 8, Statement("try/finally", lineno-11) diff --git a/tests/eval_files/async102_anyio.py b/tests/eval_files/async102_anyio.py index 61d1ea54..3583774a 100644 --- a/tests/eval_files/async102_anyio.py +++ b/tests/eval_files/async102_anyio.py @@ -1,9 +1,9 @@ # type: ignore +# this test will raise the same errors with trio, despite trio.get_cancelled_exc_class not existing +# marked not to run the tests though as error messages will only refer to anyio # NOTRIO # ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio # BASE_LIBRARY anyio -# this test will raise the same errors with trio/asyncio, despite [trio|asyncio].get_cancelled_exc_class not existing -# marked not to run the tests though as error messages will only refer to anyio import anyio from anyio import get_cancelled_exc_class diff --git a/tests/eval_files/async102_asyncio.py b/tests/eval_files/async102_asyncio.py deleted file mode 100644 index 813725d6..00000000 --- a/tests/eval_files/async102_asyncio.py +++ /dev/null @@ -1,49 +0,0 @@ -# type: ignore -# NOANYIO -# NOTRIO -# BASE_LIBRARY asyncio -# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio -from contextlib import asynccontextmanager - -import asyncio - - -async def foo(): - # asyncio.move_on_after does not exist, so this will raise an error - try: - ... - finally: - with asyncio.move_on_after(deadline=30) as s: - s.shield = True - await foo() # error: 12, Statement("try/finally", lineno-5) - - try: - pass - finally: - await foo() # error: 8, Statement("try/finally", lineno-3) - - # asyncio.CancelScope does not exist, so this will raise an error - try: - pass - finally: - with asyncio.CancelScope(deadline=30, shield=True): - await foo() # error: 12, Statement("try/finally", lineno-4) - - # TODO: I think this is the asyncio-equivalent, but functionality to ignore the error - # has not been implemented - - try: - ... - finally: - await asyncio.shield( # error: 8, Statement("try/finally", lineno-3) - asyncio.wait_for(foo()) - ) - - -# asyncio.TaskGroup *is* a source of cancellations (on exit) -async def foo_open_nursery_no_cancel(): - try: - pass - finally: - async with asyncio.TaskGroup() as tg: # error: 8, Statement("try/finally", lineno-3) - ... diff --git a/tests/eval_files/noqa_no_autofix.py b/tests/eval_files/noqa_no_autofix.py index 8897e7ce..573c9e9d 100644 --- a/tests/eval_files/noqa_no_autofix.py +++ b/tests/eval_files/noqa_no_autofix.py @@ -1,5 +1,4 @@ -# ARG --enable=ASYNC102 -# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio +# ARG --enable=ASYNC109 import trio from typing import Any @@ -9,22 +8,13 @@ async def foo() -> Any: ... -async def foo_no_noqa_102(): - try: - pass - finally: - await foo() # ASYNC102: 8, Statement("try/finally", lineno-3) +async def foo_no_noqa_109(timeout): # ASYNC109: 26, "trio" + ... -async def foo_noqa_102(): - try: - pass - finally: - await foo() # noqa: ASYNC102 +async def foo_noqa_102(timeout): # noqa: ASYNC109, "trio" + ... -async def foo_bare_noqa_102(): - try: - pass - finally: - await foo() # noqa +async def foo_bare_noqa_109(timeout): # noqa, "trio" + ... From 101779f2ae89cf80f12f9106a78447fd03d8c728 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:21:43 +0200 Subject: [PATCH 3/3] Apply suggestions from code review minor fixes --- tests/eval_files/async102.py | 5 ++--- tests/eval_files/noqa_no_autofix.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/eval_files/async102.py b/tests/eval_files/async102.py index bfc25a71..ecc749a0 100644 --- a/tests/eval_files/async102.py +++ b/tests/eval_files/async102.py @@ -312,8 +312,7 @@ async def __aexit__(): await foo() # error: 4, Statement("__aexit__", lineno-1) -# exclude finally: await x.aclose() -# trio/anyio marks arg-less aclose() as safe +# exclude `finally: await x.aclose()` with no arguments async def foo_aclose_noargs(): # no type tracking in this check, we allow any call that looks like # `await [...].aclose()` @@ -329,7 +328,7 @@ async def foo_aclose_noargs(): await x.y.aclose() -# trio/anyio should still raise errors if there's args +# should still raise errors if there's args, as that indicates it's a non-standard aclose async def foo(): # no type tracking in this check x = None diff --git a/tests/eval_files/noqa_no_autofix.py b/tests/eval_files/noqa_no_autofix.py index 573c9e9d..17740c29 100644 --- a/tests/eval_files/noqa_no_autofix.py +++ b/tests/eval_files/noqa_no_autofix.py @@ -12,9 +12,9 @@ async def foo_no_noqa_109(timeout): # ASYNC109: 26, "trio" ... -async def foo_noqa_102(timeout): # noqa: ASYNC109, "trio" +async def foo_noqa_102(timeout): # noqa: ASYNC109 ... -async def foo_bare_noqa_109(timeout): # noqa, "trio" +async def foo_bare_noqa_109(timeout): # noqa ...