Skip to content

Commit 149c535

Browse files
authored
Improve replacement (#345)
* make ASYNC912 and 913 care about cancel points, and ignore schedule points * delete now redundant eval files, fix async113 false alarm on [not trio].serve_listeners etc
1 parent 411ebb6 commit 149c535

32 files changed

+572
-463
lines changed

docs/changelog.rst

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Changelog
44

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

7+
25.2.2
8+
=======
9+
- :ref:`ASYNC113 <async113>` now only triggers on ``trio.[serve_tcp, serve_ssl_over_tcp, serve_listeners, run_process]``, instead of accepting anything as the attribute base. (e.g. :func:`anyio.run_process` is not startable).
10+
711
25.2.1
812
=======
913
- :ref:`ASYNC912 <async912>` and :ref:`ASYNC913 <async913>` will now trigger if there's no *cancel* points. This means that :func:`trio.open_nursery`/`anyio.create_task_group` will not silence them on their own, unless they're guaranteed to start tasks.

docs/usage.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ adding the following to your ``.pre-commit-config.yaml``:
3333
minimum_pre_commit_version: '2.9.0'
3434
repos:
3535
- repo: https://github.com/python-trio/flake8-async
36-
rev: 25.2.1
36+
rev: 25.2.2
3737
hooks:
3838
- id: flake8-async
3939
# args: [--enable=ASYNC, --disable=ASYNC9, --autofix=ASYNC]

flake8_async/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939

4040
# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
41-
__version__ = "25.2.1"
41+
__version__ = "25.2.2"
4242

4343

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

flake8_async/visitors/visitors.py

+25-12
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,10 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
121121
elif get_matching_call(
122122
item.context_expr, "create_task_group", base="anyio"
123123
):
124-
nursery_type = "taskgroup"
124+
nursery_type = "task group"
125125
# check for asyncio.TaskGroup
126126
elif get_matching_call(item.context_expr, "TaskGroup", base="asyncio"):
127-
nursery_type = "taskgroup"
127+
nursery_type = "task group"
128128
start_methods = ("create_task",)
129129
else:
130130
# incorrectly marked as not covered on py39
@@ -151,12 +151,12 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
151151

152152

153153
# used in 113 and 114
154-
STARTABLE_CALLS = (
154+
STARTABLE_CALLS = ("serve",)
155+
TRIO_STARTABLE_CALLS = (
155156
"run_process",
156157
"serve_ssl_over_tcp",
157158
"serve_tcp",
158159
"serve_listeners",
159-
"serve",
160160
)
161161

162162

@@ -201,7 +201,11 @@ def is_startable(n: ast.expr, *startable_list: str) -> bool:
201201
if isinstance(n, ast.Name):
202202
return n.id in startable_list
203203
if isinstance(n, ast.Attribute):
204-
return n.attr in startable_list
204+
return n.attr in startable_list and not (
205+
n.attr in TRIO_STARTABLE_CALLS
206+
and isinstance(n.value, ast.Name)
207+
and n.value.id != "trio"
208+
)
205209
if isinstance(n, ast.Call):
206210
return any(is_startable(nn, *startable_list) for nn in n.args)
207211
return False
@@ -213,12 +217,16 @@ def is_nursery_call(node: ast.expr):
213217
):
214218
return False
215219
var = ast.unparse(node.value)
216-
return ("trio" in self.library and var.endswith("nursery")) or (
217-
self.variables.get(var, "")
218-
in (
219-
"trio.Nursery",
220-
"anyio.TaskGroup",
221-
"asyncio.TaskGroup",
220+
return (
221+
("trio" in self.library and var.endswith("nursery"))
222+
or ("anyio" in self.library and var.endswith("task_group"))
223+
or (
224+
self.variables.get(var, "")
225+
in (
226+
"trio.Nursery",
227+
"anyio.TaskGroup",
228+
"asyncio.TaskGroup",
229+
)
222230
)
223231
)
224232

@@ -229,6 +237,7 @@ def is_nursery_call(node: ast.expr):
229237
and is_startable(
230238
node.args[0],
231239
*STARTABLE_CALLS,
240+
*TRIO_STARTABLE_CALLS,
232241
*self.options.startable_in_context_manager,
233242
)
234243
):
@@ -254,7 +263,11 @@ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
254263
for n in self.walk(*node.args.args, *node.args.kwonlyargs)
255264
) and not any(
256265
node.name == opt
257-
for opt in (*self.options.startable_in_context_manager, *STARTABLE_CALLS)
266+
for opt in (
267+
*self.options.startable_in_context_manager,
268+
*STARTABLE_CALLS,
269+
*TRIO_STARTABLE_CALLS,
270+
)
258271
):
259272
self.error(node, node.name)
260273

tests/autofix_files/async100.py

+74
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,77 @@ async def dont_crash_on_non_name_or_attr_call():
140140
async def another_weird_with_call():
141141
async with a().b():
142142
...
143+
144+
145+
# ---open_nursery / create_task_group stuff---
146+
147+
148+
async def nursery_no_cancel_point():
149+
# error: 9, "trio", "CancelScope"
150+
async with trio.open_nursery():
151+
...
152+
153+
154+
# but it is a cancel point if the nursery contains a call to start_soon()
155+
156+
157+
async def nursery_start_soon():
158+
with trio.CancelScope():
159+
async with trio.open_nursery() as n:
160+
n.start_soon(trio.sleep, 0)
161+
162+
163+
async def nursery_start_soon_misnested():
164+
async with trio.open_nursery() as n:
165+
# error: 13, "trio", "CancelScope"
166+
n.start_soon(trio.sleep, 0)
167+
168+
169+
async def nested_scope():
170+
with trio.CancelScope():
171+
with trio.CancelScope():
172+
async with trio.open_nursery() as n:
173+
n.start_soon(trio.sleep, 0)
174+
175+
176+
async def nested_nursery():
177+
with trio.CancelScope():
178+
async with trio.open_nursery() as n:
179+
async with trio.open_nursery() as n2:
180+
n2.start_soon(trio.sleep, 0)
181+
182+
183+
async def nested_function_call():
184+
185+
# error: 9, "trio", "CancelScope"
186+
async with trio.open_nursery() as n:
187+
188+
def foo():
189+
n.start_soon(trio.sleep, 0)
190+
191+
# a false alarm in case we call foo()... but we can't check if they do
192+
foo()
193+
194+
195+
# insert cancel point on nursery exit, not at the start_soon call
196+
async def cancel_point_on_nursery_exit():
197+
with trio.CancelScope():
198+
async with trio.open_nursery() as n:
199+
# error: 17, "trio", "CancelScope"
200+
n.start_soon(trio.sleep, 0)
201+
202+
203+
# async100 does not consider *redundant* cancel scopes
204+
async def redundant_cancel_scope():
205+
with trio.CancelScope():
206+
with trio.CancelScope():
207+
await trio.lowlevel.checkpoint()
208+
209+
210+
# but if it did then none of these scopes should be marked redundant
211+
# The inner checks task startup, the outer checks task exit
212+
async def nursery_exit_blocks_with_start():
213+
with trio.CancelScope():
214+
async with trio.open_nursery() as n:
215+
with trio.CancelScope():
216+
await n.start(trio.sleep, 0)

tests/autofix_files/async100.py.diff

+64-5
Original file line numberDiff line numberDiff line change
@@ -96,14 +96,15 @@
9696
await trio.sleep_forever()
9797
- with trio.CancelScope(): # error: 13, "trio", "CancelScope"
9898
- ...
99-
+ # error: 13, "trio", "CancelScope"
100-
+ ...
101-
99+
-
102100
- with trio.fail_after(1): # error: 9, "trio", "fail_after"
103101
- with trio.CancelScope(): # error: 13, "trio", "CancelScope"
104102
- ...
105103
- with trio.CancelScope(): # error: 13, "trio", "CancelScope"
106104
- ...
105+
+ # error: 13, "trio", "CancelScope"
106+
+ ...
107+
+
107108
+ # error: 9, "trio", "fail_after"
108109
+ # error: 13, "trio", "CancelScope"
109110
+ ...
@@ -119,14 +120,72 @@
119120
- with trio.fail_after(1): # error: 9, "trio", "fail_after"
120121
- with contextlib.suppress(Exception):
121122
- print("foo")
122-
+ # error: 9, "trio", "fail_after"
123-
with contextlib.suppress(Exception):
123+
- with contextlib.suppress(Exception):
124124
- with trio.fail_after(1): # error: 13, "trio", "fail_after"
125125
- print("foo")
126+
+ # error: 9, "trio", "fail_after"
127+
+ with contextlib.suppress(Exception):
126128
+ print("foo")
127129
+ with contextlib.suppress(Exception):
128130
+ # error: 13, "trio", "fail_after"
129131
+ print("foo")
130132

131133
with contextlib.suppress(Exception):
132134
with open("blah") as file:
135+
@@ x,9 x,9 @@
136+
137+
138+
async def nursery_no_cancel_point():
139+
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
140+
- async with trio.open_nursery():
141+
- ...
142+
+ # error: 9, "trio", "CancelScope"
143+
+ async with trio.open_nursery():
144+
+ ...
145+
146+
147+
# but it is a cancel point if the nursery contains a call to start_soon()
148+
@@ x,8 x,8 @@
149+
150+
async def nursery_start_soon_misnested():
151+
async with trio.open_nursery() as n:
152+
- with trio.CancelScope(): # error: 13, "trio", "CancelScope"
153+
- n.start_soon(trio.sleep, 0)
154+
+ # error: 13, "trio", "CancelScope"
155+
+ n.start_soon(trio.sleep, 0)
156+
157+
158+
async def nested_scope():
159+
@@ x,22 x,22 @@
160+
161+
async def nested_function_call():
162+
163+
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
164+
- async with trio.open_nursery() as n:
165+
-
166+
- def foo():
167+
- n.start_soon(trio.sleep, 0)
168+
-
169+
- # a false alarm in case we call foo()... but we can't check if they do
170+
- foo()
171+
+ # error: 9, "trio", "CancelScope"
172+
+ async with trio.open_nursery() as n:
173+
+
174+
+ def foo():
175+
+ n.start_soon(trio.sleep, 0)
176+
+
177+
+ # a false alarm in case we call foo()... but we can't check if they do
178+
+ foo()
179+
180+
181+
# insert cancel point on nursery exit, not at the start_soon call
182+
async def cancel_point_on_nursery_exit():
183+
with trio.CancelScope():
184+
async with trio.open_nursery() as n:
185+
- with trio.CancelScope(): # error: 17, "trio", "CancelScope"
186+
- n.start_soon(trio.sleep, 0)
187+
+ # error: 17, "trio", "CancelScope"
188+
+ n.start_soon(trio.sleep, 0)
189+
190+
191+
# async100 does not consider *redundant* cancel scopes

tests/autofix_files/async100_anyio.py

-26
This file was deleted.

tests/autofix_files/async100_anyio.py.diff

-23
This file was deleted.

0 commit comments

Comments
 (0)