Skip to content

Commit fa6bdbb

Browse files
committed
make ASYNC912 and 913 care about cancel points, and ignore schedule points
1 parent 9bf1d71 commit fa6bdbb

11 files changed

+67
-11
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.1
8+
=======
9+
- :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.
10+
711
25.1.1
812
=======
913
- Add :ref:`ASYNC124 <async124>` async-function-could-be-sync

docs/rules.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -197,12 +197,13 @@ _`ASYNC911` : async-generator-no-checkpoint
197197
Exit, ``yield`` or ``return`` from async iterable with no guaranteed :ref:`checkpoint` since possible function entry (``yield`` or function definition).
198198

199199
_`ASYNC912` : cancel-scope-no-guaranteed-checkpoint
200-
A timeout/cancelscope has :ref:`checkpoints <checkpoint>`, but they're not guaranteed to run.
201-
Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no checkpoint at all.
200+
A timeout/cancelscope has :ref:`cancel points <cancel_point>`, but they're not guaranteed to run.
201+
Similar to `ASYNC100`_, but it does not warn on trivial cases where there is no cancel point at all.
202202
It instead shares logic with `ASYNC910`_ and `ASYNC911`_ for parsing conditionals and branches.
203203

204204
_`ASYNC913` : indefinite-loop-no-guaranteed-checkpoint
205205
An indefinite loop (e.g. ``while True``) has no guaranteed :ref:`checkpoint <checkpoint>`. This could potentially cause a deadlock.
206+
This will also error if there's no guaranteed :ref:`cancel point`, where even though it won't deadlock the loop might become an uncancelable dry-run loop.
206207

207208
.. _autofix-support:
208209

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.1.1
36+
rev: 25.2.1
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.1.1"
41+
__version__ = "25.2.1"
4242

4343

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

flake8_async/visitors/visitor91x.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -354,10 +354,10 @@ class Visitor91X(Flake8AsyncVisitor_cst, CommonVisitors):
354354
"on line {1.lineno}."
355355
),
356356
"ASYNC912": (
357-
"CancelScope with no guaranteed checkpoint. This makes it potentially "
357+
"CancelScope with no guaranteed cancel point. This makes it potentially "
358358
"impossible to cancel."
359359
),
360-
"ASYNC913": ("Indefinite loop with no guaranteed checkpoint."),
360+
"ASYNC913": ("Indefinite loop with no guaranteed cancel points."),
361361
"ASYNC100": (
362362
"{0}.{1} context contains no checkpoints, remove the context or add"
363363
" `await {0}.lowlevel.checkpoint()`."
@@ -401,10 +401,16 @@ def checkpoint_cancel_point(self) -> None:
401401
self.taskgroup_has_start_soon.clear()
402402

403403
def checkpoint_schedule_point(self) -> None:
404-
self.uncheckpointed_statements = set()
404+
# ASYNC912&ASYNC913 only cares about cancel points, so don't remove
405+
# them if we only do a schedule point
406+
self.uncheckpointed_statements = {
407+
s
408+
for s in self.uncheckpointed_statements
409+
if isinstance(s, ArtificialStatement)
410+
}
405411

406412
def checkpoint(self) -> None:
407-
self.checkpoint_schedule_point()
413+
self.uncheckpointed_statements = set()
408414
self.checkpoint_cancel_point()
409415

410416
def checkpoint_statement(self) -> cst.SimpleStatementLine:
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ARG --enable=ASYNC913
2+
# AUTOFIX
3+
# ASYNCIO_NO_ERROR
4+
5+
import trio
6+
7+
8+
async def nursery_no_cancel_point():
9+
while True: # ASYNC913: 4
10+
await trio.lowlevel.checkpoint()
11+
async with trio.open_nursery():
12+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
+++
3+
@@ x,5 x,6 @@
4+
5+
async def nursery_no_cancel_point():
6+
while True: # ASYNC913: 4
7+
+ await trio.lowlevel.checkpoint()
8+
async with trio.open_nursery():
9+
pass

tests/eval_files/async912.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def customWrapper(a: T) -> T:
125125
with (res := trio.fail_at(10)):
126126
...
127127
# but saving with `as` does
128-
with trio.fail_at(10) as res: # ASYNC912: 9
128+
with trio.fail_at(10) as res2: # ASYNC912: 9
129129
if bar():
130130
await trio.lowlevel.checkpoint()
131131

@@ -189,3 +189,10 @@ async def check_yield_logic():
189189
if bar():
190190
await trio.lowlevel.checkpoint()
191191
yield
192+
193+
194+
async def nursery_no_cancel_point():
195+
with trio.move_on_after(10): # ASYNC912: 9
196+
async with trio.open_nursery():
197+
if bar():
198+
await trio.lowlevel.checkpoint()
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# ARG --enable=ASYNC913
2+
# AUTOFIX
3+
# ASYNCIO_NO_ERROR
4+
5+
import trio
6+
7+
8+
async def nursery_no_cancel_point():
9+
while True: # ASYNC913: 4
10+
async with trio.open_nursery():
11+
pass

tests/test_flake8_async.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,14 @@ def diff_strings(first: str, second: str, /) -> str:
103103
# replaces all instances of `original` with `new` in string
104104
# unless it's preceded by a `-`, which indicates it's part of a command-line flag
105105
def replace_library(string: str, original: str = "trio", new: str = "anyio") -> str:
106-
return re.sub(rf"(?<!-){original}", new, string)
106+
def replace_str(string: str, original: str, new: str) -> str:
107+
return re.sub(rf"(?<!-){original}", new, string)
108+
109+
if original == "trio" and new == "anyio":
110+
string = replace_str(string, "trio.open_nursery", "anyio.create_task_group")
111+
elif original == "anyio" and new == "trio":
112+
string = replace_str(string, "anyio.create_task_group", "trio.open_nursery")
113+
return replace_str(string, original, new)
107114

108115

109116
def check_autofix(

tox.ini

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ commands =
3535
# Settings for other tools
3636
[pytest]
3737
addopts =
38-
--tb=native
3938
--cov=flake8_async
4039
--cov-branch
4140
--cov-report=term-missing:skip-covered

0 commit comments

Comments
 (0)