diff --git a/docs/changelog.rst b/docs/changelog.rst
index 26b10c5..1064da0 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 4a640aa..afc7943 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 b1bb6c2..4682799 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 c0ba7b3..89eb93d 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 1187e27..1a1dc77 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 3d3f6d6..ecc749a 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
@@ -310,3 +310,38 @@ 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()` with no arguments
+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()
+
+
+# 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
+
+ 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 4cba907..0000000
--- a/tests/eval_files/async102_aclose.py
+++ /dev/null
@@ -1,26 +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
-
-# 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 6db0d9f..0000000
--- a/tests/eval_files/async102_aclose_args.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# type: ignore
-
-# 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 f31eb11..3583774 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
-# 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
diff --git a/tests/eval_files/async102_asyncio.py b/tests/eval_files/async102_asyncio.py
deleted file mode 100644
index 5dabb98..0000000
--- a/tests/eval_files/async102_asyncio.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# type: ignore
-# NOANYIO
-# NOTRIO
-# BASE_LIBRARY 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/async102_trio.py b/tests/eval_files/async102_trio.py
index 7f1a5b9..d956c91 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 36f98bf..17740c2 100644
--- a/tests/eval_files/noqa_no_autofix.py
+++ b/tests/eval_files/noqa_no_autofix.py
@@ -1,4 +1,4 @@
-# ARG --enable=ASYNC102
+# ARG --enable=ASYNC109
import trio
from typing import Any
@@ -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
+ ...
-async def foo_bare_noqa_102():
- try:
- pass
- finally:
- await foo() # noqa
+async def foo_bare_noqa_109(timeout): # noqa
+ ...