Skip to content

Commit 8719f3f

Browse files
authored
ASYNC102 no longer raises warning for calls to .aclose() on objects (#222)
* ASYNC102 no longer raises warning for calls to `.aclose()` on objects * bump __version__ * fixes after review: allow aclose() in except clauses. Only allow if no args. Don't exempt asyncio-only code. * lol I was so confused why this started failing
1 parent 219c6d3 commit 8719f3f

File tree

7 files changed

+96
-8
lines changed

7 files changed

+96
-8
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# Changelog
22
*[CalVer, YY.month.patch](https://calver.org/)*
33

4+
## 24.3.5
5+
- ASYNC102 (no await inside finally or critical except) no longer raises warnings for calls to `aclose()` on objects in trio/anyio code. See https://github.com/python-trio/flake8-async/issues/156
6+
47
## 24.3.4
5-
- ASYNC110 (don't loop sleep) now also warns if looping `[trio/anyio].lowlevel.checkpoint()`
8+
- ASYNC110 (don't loop sleep) now also warns if looping `[trio/anyio].lowlevel.checkpoint()`.
69

710
## 24.3.3
811
- Add ASYNC251: `time.sleep()` in async method.

flake8_async/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
40-
__version__ = "24.3.4"
40+
__version__ = "24.3.5"
4141

4242

4343
# taken from https://github.com/Zac-HD/shed

flake8_async/visitors/visitor102.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,35 @@ def __init__(self, *args: Any, **kwargs: Any):
5252
self._trio_context_managers: list[Visitor102.TrioScope] = []
5353
self.cancelled_caught = False
5454

55-
# if we're inside a finally, and we're not inside a scope that doesn't have
56-
# both a timeout and shield
57-
def visit_Await(self, node: ast.Await | ast.AsyncFor | ast.AsyncWith):
55+
# if we're inside a finally or critical except, and we're not inside a scope that
56+
# doesn't have both a timeout and shield
57+
def async_call_checker(
58+
self, node: ast.Await | ast.AsyncFor | ast.AsyncWith
59+
) -> None:
5860
if self._critical_scope is not None and not any(
5961
cm.has_timeout and cm.shielded for cm in self._trio_context_managers
6062
):
6163
self.error(node, self._critical_scope)
6264

63-
visit_AsyncFor = visit_Await
65+
def is_safe_aclose_call(self, node: ast.Await) -> bool:
66+
return (
67+
# don't mark calls safe in asyncio-only files
68+
# a more defensive option would be `asyncio not in self.library`
69+
self.library != ("asyncio",)
70+
and isinstance(node.value, ast.Call)
71+
# only known safe if no arguments
72+
and not node.value.args
73+
and not node.value.keywords
74+
and isinstance(node.value.func, ast.Attribute)
75+
and node.value.func.attr == "aclose"
76+
)
77+
78+
def visit_Await(self, node: ast.Await):
79+
# allow calls to `.aclose()`
80+
if not (self.is_safe_aclose_call(node)):
81+
self.async_call_checker(node)
82+
83+
visit_AsyncFor = async_call_checker
6484

6585
def visit_With(self, node: ast.With | ast.AsyncWith):
6686
self.save_state(node, "_trio_context_managers", copy=True)
@@ -82,7 +102,7 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
82102
break
83103

84104
def visit_AsyncWith(self, node: ast.AsyncWith):
85-
self.visit_Await(node)
105+
self.async_call_checker(node)
86106
self.visit_With(node)
87107

88108
def visit_Try(self, node: ast.Try):
@@ -94,6 +114,10 @@ def visit_Try(self, node: ast.Try):
94114
self.visit_nodes(node.body, node.handlers, node.orelse)
95115

96116
self._trio_context_managers = []
117+
# node.finalbody does not have a lineno, so we give the position of the try
118+
# it'd be possible to estimate the lineno given the last except and the first
119+
# statement in the finally, but it would be very hard to get it perfect with
120+
# comments and empty lines and stuff.
97121
self._critical_scope = Statement("try/finally", node.lineno, node.col_offset)
98122
self.visit_nodes(node.finalbody)
99123

tests/eval_files/async102.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,16 @@ async def foo_nested_excepts():
252252
await foo()
253253
await foo()
254254
await foo()
255+
256+
257+
# double check that this works nested inside an async for
258+
async def foo_nested_async_for():
259+
260+
async for i in trio.bypasslinters:
261+
try:
262+
...
263+
except BaseException:
264+
async for ( # error: 12, Statement("BaseException", lineno-1)
265+
j
266+
) in trio.bypasslinters:
267+
...

tests/eval_files/async102_aclose.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# type: ignore
2+
3+
# exclude finally: await x.aclose() from async102, with trio/anyio
4+
# ANYIO_NO_ERROR
5+
# TRIO_NO_ERROR
6+
# See also async102_aclose_args.py - which makes sure trio/anyio raises errors if there
7+
# are arguments to aclose().
8+
9+
10+
async def foo():
11+
# no type tracking in this check, we allow any call that looks like
12+
# `await [...].aclose()`
13+
x = None
14+
15+
try:
16+
...
17+
except BaseException:
18+
await x.aclose() # ASYNC102: 8, Statement("BaseException", lineno-1)
19+
await x.y.aclose() # ASYNC102: 8, Statement("BaseException", lineno-2)
20+
finally:
21+
await x.aclose() # ASYNC102: 8, Statement("try/finally", lineno-6)
22+
await x.y.aclose() # ASYNC102: 8, Statement("try/finally", lineno-7)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# type: ignore
2+
3+
# trio/anyio should still raise errors if there's args
4+
# asyncio will always raise errors
5+
6+
# See also async102_aclose.py, which checks that trio/anyio marks arg-less aclose() as safe
7+
8+
9+
async def foo():
10+
# no type tracking in this check
11+
x = None
12+
13+
try:
14+
...
15+
except BaseException:
16+
await x.aclose(foo) # ASYNC102: 8, Statement("BaseException", lineno-1)
17+
await x.aclose(bar=foo) # ASYNC102: 8, Statement("BaseException", lineno-2)
18+
await x.aclose(*foo) # ASYNC102: 8, Statement("BaseException", lineno-3)
19+
await x.aclose(None) # ASYNC102: 8, Statement("BaseException", lineno-4)
20+
finally:
21+
await x.aclose(foo) # ASYNC102: 8, Statement("try/finally", lineno-8)
22+
await x.aclose(bar=foo) # ASYNC102: 8, Statement("try/finally", lineno-9)
23+
await x.aclose(*foo) # ASYNC102: 8, Statement("try/finally", lineno-10)
24+
await x.aclose(None) # ASYNC102: 8, Statement("try/finally", lineno-11)

tests/test_messages_documented.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ def test_messages_documented():
4444
if not file_path.is_file():
4545
continue
4646

47-
if m := re.search(r"async\d\d\d", str(file_path)):
47+
# only look in the stem (final part of the path), so as not to get tripped
48+
# up by [git worktree] directories with an exception code in the name
49+
if m := re.search(r"^async\d\d\d", str(file_path.stem)):
4850
documented_errors["eval_files"].add(m.group().upper())
4951

5052
with open(file_path) as file:

0 commit comments

Comments
 (0)