Skip to content

async102 not applicable to asyncio #393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 29, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog

`CalVer, YY.month.patch <https://calver.org/>`_

25.7.1
======
- :ref:`ASYNC102 <async102>` no longer triggered for asyncio due to different cancellation semantics it uses.

25.5.3
======
- :ref:`ASYNC115 <async115>` and :ref:`ASYNC116 <async116>` now also checks kwargs.
Expand Down
2 changes: 1 addition & 1 deletion docs/rules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ _`ASYNC101` : yield-in-cancel-scope
_`ASYNC102` : await-in-finally-or-cancelled
``await`` inside ``finally``, :ref:`cancelled-catching <cancelled>` ``except:``, or ``__aexit__`` must have shielded :ref:`cancel scope <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 <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.
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
2 changes: 1 addition & 1 deletion flake8_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 3 additions & 5 deletions flake8_async/visitors/visitor102_120.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines 75 to 79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASYNC120 has the same issue as ASYNC102 wrt asyncio, so if you don't mind you can expand the scope of this PR to also cover async120 with a small logic change here.

Copy link
Contributor Author

@RomanValov RomanValov Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as per my understanding async120 is still relevant to asyncio.
in case of async102 -- if cancellation occured in try block, it remains in except/finally blocks with trio/anyio
in case of async120 -- it only checks cancellation occurs during except ... block which is still valid for asyncio
please correct me if I'm missing something

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry no, you're totally right. Ignore me


def visit_Raise(self, node: ast.Raise):
Expand All @@ -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
Expand Down
38 changes: 37 additions & 1 deletion tests/eval_files/async102.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
26 changes: 0 additions & 26 deletions tests/eval_files/async102_aclose.py

This file was deleted.

24 changes: 0 additions & 24 deletions tests/eval_files/async102_aclose_args.py

This file was deleted.

6 changes: 3 additions & 3 deletions tests/eval_files/async102_anyio.py
Original file line number Diff line number Diff line change
@@ -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
# 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
import anyio
from anyio import get_cancelled_exc_class

Expand Down
48 changes: 0 additions & 48 deletions tests/eval_files/async102_asyncio.py

This file was deleted.

2 changes: 1 addition & 1 deletion tests/eval_files/async102_trio.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# NOASYNCIO
# NOANYIO - since anyio.Cancelled does not exist
# ASYNCIO_NO_ERROR # ASYNC102 not applicable to asyncio
import trio


Expand Down
23 changes: 7 additions & 16 deletions tests/eval_files/noqa_no_autofix.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# ARG --enable=ASYNC102
# ARG --enable=ASYNC109

import trio
from typing import Any
Expand All @@ -8,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"
...
Loading