Skip to content

Commit 22f9ba8

Browse files
committed
Exempt aclose_forcefully from ASYNC102
`trio.aclose_forcefully` (and `anyio.aclose_forcefully`) are designed specifically for cleanup and cancel immediately by design, so calling them in a `finally:` / critical `except:` does not introduce the cancellation-replacing hazard that ASYNC102 targets. Treat them as safe, same as `.aclose()` with no arguments. Fixes #446.
1 parent 22f9e27 commit 22f9ba8

4 files changed

Lines changed: 33 additions & 7 deletions

File tree

docs/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Unreleased
1010
- :ref:`ASYNC910 <async910>` and :ref:`ASYNC911 <async911>` now accept ``__aenter__`` / ``__aexit__`` methods when the partner method provides the checkpoint, or when only one of the two is defined on a class that inherits from another class (charitably assuming the partner is inherited and contains a checkpoint). `(issue #441) <https://github.com/python-trio/flake8-async/issues/441>`_
1111
- :ref:`ASYNC300 <async300>` no longer triggers when the result of ``asyncio.create_task()`` is returned from a function. `(issue #398) <https://github.com/python-trio/flake8-async/issues/398>`_
1212
- Add :ref:`ASYNC126 <async126>` exceptiongroup-subclass-missing-derive. `(issue #334) <https://github.com/python-trio/flake8-async/issues/334>`_
13+
- :ref:`ASYNC102 <async102>` no longer warns on ``await trio.aclose_forcefully(...)`` / ``await anyio.aclose_forcefully(...)``, which are designed for cleanup and cancel immediately by design. `(issue #446) <https://github.com/python-trio/flake8-async/issues/446>`_
1314

1415
25.7.1
1516
======

docs/rules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ _`ASYNC102` : await-in-finally-or-cancelled
2525
``await`` inside ``finally``, :ref:`cancelled-catching <cancelled>` ``except:``, or ``__aexit__`` must have shielded :ref:`cancel scope <cancel_scope>` with timeout.
2626
If not, the async call will immediately raise a new cancellation, suppressing any cancellation that was caught.
2727
Not applicable to asyncio due to edge-based cancellation semantics it uses as opposed to level-based used by trio and anyio.
28+
Calls to ``.aclose()`` (with no arguments) and to :func:`trio.aclose_forcefully` / :func:`anyio.aclose_forcefully` are exempt, as they are intended for use in cleanup.
2829
See :ref:`ASYNC120 <async120>` for the general case where other exceptions might get suppressed.
2930

3031
ASYNC103 : no-reraise-cancelled

flake8_async/visitors/visitor102_120.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,22 @@ def visit_Raise(self, node: ast.Raise):
8484
self._potential_120.clear()
8585

8686
def is_safe_aclose_call(self, node: ast.Await) -> bool:
87-
return (
88-
isinstance(node.value, ast.Call)
89-
# only known safe if no arguments
87+
if not isinstance(node.value, ast.Call):
88+
return False
89+
# allow `<x>.aclose()` with no arguments
90+
if (
91+
isinstance(node.value.func, ast.Attribute)
92+
and node.value.func.attr == "aclose"
9093
and not node.value.args
9194
and not node.value.keywords
92-
and isinstance(node.value.func, ast.Attribute)
93-
and node.value.func.attr == "aclose"
94-
)
95+
):
96+
return True
97+
# allow `trio.aclose_forcefully(<x>)` / `anyio.aclose_forcefully(<x>)`,
98+
# which are specifically designed for cleanup and cancel immediately by design
99+
return get_matching_call(node.value, "aclose_forcefully") is not None
95100

96101
def visit_Await(self, node: ast.Await):
97-
# allow calls to `.aclose()`
102+
# allow calls to `.aclose()` and `[trio/anyio].aclose_forcefully(...)`
98103
if not (self.is_safe_aclose_call(node)):
99104
self.async_call_checker(node)
100105

tests/eval_files/async102.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,22 @@ async def foo():
345345
await x.aclose(bar=foo) # ASYNC102: 8, Statement("try/finally", lineno-9)
346346
await x.aclose(*foo) # ASYNC102: 8, Statement("try/finally", lineno-10)
347347
await x.aclose(None) # ASYNC102: 8, Statement("try/finally", lineno-11)
348+
349+
350+
# aclose_forcefully is designed for cleanup and is safe in finally/except
351+
# see https://github.com/python-trio/flake8-async/issues/446
352+
async def foo_aclose_forcefully():
353+
x = None
354+
355+
try:
356+
...
357+
except BaseException:
358+
await trio.aclose_forcefully(x)
359+
finally:
360+
await trio.aclose_forcefully(x)
361+
362+
# unqualified or unknown-base call is still treated as unsafe
363+
try:
364+
...
365+
finally:
366+
await aclose_forcefully(x) # ASYNC102: 8, Statement("try/finally", lineno-3)

0 commit comments

Comments
 (0)