Skip to content

Commit 7a45176

Browse files
jakkdlZac-HD
andauthored
async100 now ignores trio.open_nursery and anyio.create_task_group (#317)
* async100 now ignores trio.open_nursery and anyio.create_task_group * don't crash on weird 'with' call * add more documentation. fix bad formatting for 24.9.3 in changelog * tweak docs * fix cross-refs --------- Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
1 parent 4fa719f commit 7a45176

11 files changed

Lines changed: 136 additions & 10 deletions

File tree

docs/changelog.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44

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

7+
24.11.1
8+
=======
9+
- :ref:`ASYNC100 <async100>` now ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group`
10+
as cancellation sources, because they are :ref:`schedule points <schedule_points>` but not
11+
:ref:`cancellation points <cancel_points>`.
12+
713
24.10.2
814
=======
915
- :ref:`ASYNC101 <async101>` and :ref:`ASYNC119 <async119>` are now silenced for decorators in :ref:`transform-async-generator-decorators`
@@ -24,6 +30,7 @@ Changelog
2430
24.9.3
2531
======
2632
- :ref:`ASYNC102 <async102>` and :ref:`ASYNC120 <async120>`:
33+
2734
- handles nested cancel scopes
2835
- detects internal cancel scopes of nurseries as a way to shield&deadline
2936
- no longer treats :func:`trio.open_nursery` or :func:`anyio.create_task_group` as cancellation sources

docs/glossary.rst

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ Exception classes:
8888

8989
Checkpoint
9090
----------
91-
Checkpoints are points where the async backend checks for cancellation and
92-
can switch which task is running, in an ``await``, ``async for``, or ``async with``
91+
Checkpoints are points where the async backend checks for :ref:`cancellation <cancel_point>` and
92+
:ref:`can switch which task is running <schedule_point>`, in an ``await``, ``async for``, or ``async with``
9393
expression. Regular checkpoints can be important for both performance and correctness.
9494

9595
Trio has extensive and detailed documentation on the concept of
@@ -99,11 +99,11 @@ functions defined by Trio will either checkpoint or raise an exception when
9999
iteration, and when exhausting the iterator, and ``async with`` will checkpoint
100100
on at least one of enter/exit.
101101

102+
The one exception is :func:`trio.open_nursery` and :func:`anyio.create_task_group` which are :ref:`schedule_points` but not :ref:`cancel_points`.
103+
102104
asyncio does not place any guarantees on if or when asyncio functions will
103105
checkpoint. This means that enabling and adhering to :ref:`ASYNC91x <ASYNC910>`
104-
will still not guarantee checkpoints.
105-
106-
For anyio it will depend on the current backend.
106+
will still not guarantee checkpoints on asyncio (even if used via anyio).
107107

108108
When using Trio (or an AnyIO library that people might use on Trio), it can be
109109
very helpful to ensure that your own code adheres to the same guarantees as
@@ -116,6 +116,35 @@ To insert a checkpoint with no other side effects, you can use
116116
:func:`trio.lowlevel.checkpoint`/:func:`anyio.lowlevel.checkpoint`/:func:`asyncio.sleep(0)
117117
<asyncio.sleep>`
118118

119+
.. _schedule_point:
120+
.. _schedule_points:
121+
122+
Schedule Point
123+
--------------
124+
A schedule point is half of a full :ref:`checkpoint`, which allows the async backend to switch the running task, but doesn't check for cancellation (the other half is a :ref:`cancel_point`).
125+
While you are unlikely to need one, they are available as :func:`trio.lowlevel.cancel_shielded_checkpoint`/:func:`anyio.lowlevel.cancel_shielded_checkpoint`, and equivalent to
126+
127+
.. code-block:: python
128+
129+
from trio import CancelScope, lowlevel
130+
# or
131+
# from anyio import CancelScope, lowlevel
132+
133+
with CancelScope(shield=True):
134+
await lowlevel.checkpoint()
135+
136+
asyncio does not have any direct equivalents due to their cancellation model being different.
137+
138+
139+
.. _cancel_point:
140+
.. _cancel_points:
141+
142+
Cancel Point
143+
------------
144+
A schedule point is half of a full :ref:`checkpoint`, which will raise :ref:`cancelled` if the enclosing cancel scope has been cancelled, but does not allow the scheduler to switch to a different task (the other half is a :ref:`schedule_point`).
145+
While you are unlikely to need one, they are available as :func:`trio.lowlevel.checkpoint_if_cancelled`/:func:`anyio.lowlevel.checkpoint_if_cancelled`.
146+
Users of asyncio might want to use :meth:`asyncio.Task.cancelled`.
147+
119148
.. _channel_stream_queue:
120149

121150
Channel / Stream / Queue

docs/rules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ _`ASYNC100` : cancel-scope-no-checkpoint
1313
A :ref:`timeout_context` does not contain any :ref:`checkpoints <checkpoint>`.
1414
This makes it pointless, as the timeout can only be triggered by a checkpoint.
1515
This check also treats ``yield`` as a checkpoint, since checkpoints can happen in the caller we yield to.
16+
:func:`trio.open_nursery` and :func:`anyio.create_task_group` are excluded, as they are :ref:`schedule_points` but not :ref:`cancel_points`.
1617
See :ref:`ASYNC912 <async912>` which will in addition guarantee checkpoints on every code path.
1718

1819
_`ASYNC101` : yield-in-cancel-scope

flake8_async/__init__.py

Lines changed: 1 addition & 1 deletion
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__ = "24.10.2"
41+
__version__ = "24.11.1"
4242

4343

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

flake8_async/visitors/visitor91x.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
flatten_preserving_comments,
2626
fnmatch_qualified_name_cst,
2727
func_has_decorator,
28+
identifier_to_string,
2829
iter_guaranteed_once_cst,
2930
with_has_call,
3031
)
@@ -491,12 +492,34 @@ def _is_exception_suppressing_context_manager(self, node: cst.With) -> bool:
491492
is not None
492493
)
493494

495+
def _checkpoint_with(self, node: cst.With):
496+
"""Conditionally checkpoints entry/exit of With.
497+
498+
If the with only contains calls to open_nursery/create_task_group, it's a schedule
499+
point but not a cancellation point, so we treat it as a checkpoint for async91x
500+
but not for async100.
501+
"""
502+
if getattr(node, "asynchronous", None):
503+
for item in node.items:
504+
if not isinstance(item.item, cst.Call) or not isinstance(
505+
item.item.func, (cst.Attribute, cst.Name)
506+
):
507+
self.checkpoint()
508+
break
509+
510+
func = identifier_to_string(item.item.func)
511+
assert func is not None
512+
if func not in ("trio.open_nursery", "anyio.create_task_group"):
513+
self.checkpoint()
514+
break
515+
else:
516+
self.uncheckpointed_statements = set()
517+
494518
# Async context managers can reasonably checkpoint on either or both of entry and
495519
# exit. Given that we can't tell which, we assume "both" to avoid raising a
496520
# missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
497521
def visit_With_body(self, node: cst.With):
498-
if getattr(node, "asynchronous", None):
499-
self.checkpoint()
522+
self._checkpoint_with(node)
500523

501524
# if this might suppress exceptions, we cannot treat anything inside it as
502525
# checkpointing.
@@ -555,8 +578,7 @@ def leave_With(self, original_node: cst.With, updated_node: cst.With):
555578
self.restore_state(original_node)
556579
self.uncheckpointed_statements.update(prev_checkpoints)
557580

558-
if getattr(original_node, "asynchronous", None):
559-
self.checkpoint()
581+
self._checkpoint_with(original_node)
560582
return updated_node
561583

562584
# error if no checkpoint since earlier yield or function entry

tests/autofix_files/async100.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,14 @@ async def fn(timeout):
130130
if condition():
131131
return
132132
await trio.sleep(1)
133+
134+
135+
async def nursery_no_cancel_point():
136+
# error: 9, "trio", "CancelScope"
137+
async with anyio.create_task_group():
138+
...
139+
140+
141+
async def dont_crash_on_non_name_or_attr_call():
142+
async with contextlib.asynccontextmanager(agen_fn)():
143+
...

tests/autofix_files/async100.py.diff

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,16 @@
130130

131131
with contextlib.suppress(Exception):
132132
with open("blah") as file:
133+
@@ x,9 x,9 @@
134+
135+
136+
async def nursery_no_cancel_point():
137+
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
138+
- async with anyio.create_task_group():
139+
- ...
140+
+ # error: 9, "trio", "CancelScope"
141+
+ async with anyio.create_task_group():
142+
+ ...
143+
144+
145+
async def dont_crash_on_non_name_or_attr_call():
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# AUTOFIX
2+
# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist
3+
# ANYIO_NO_ERROR # anyio.open_nursery doesn't exist
4+
import trio
5+
6+
7+
async def nursery_no_cancel_point():
8+
# error: 9, "trio", "CancelScope"
9+
async with trio.open_nursery():
10+
...
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
+++
3+
@@ x,6 x,6 @@
4+
5+
6+
async def nursery_no_cancel_point():
7+
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
8+
- async with trio.open_nursery():
9+
- ...
10+
+ # error: 9, "trio", "CancelScope"
11+
+ async with trio.open_nursery():
12+
+ ...

tests/eval_files/async100.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,14 @@ async def fn(timeout):
130130
if condition():
131131
return
132132
await trio.sleep(1)
133+
134+
135+
async def nursery_no_cancel_point():
136+
with trio.CancelScope(): # error: 9, "trio", "CancelScope"
137+
async with anyio.create_task_group():
138+
...
139+
140+
141+
async def dont_crash_on_non_name_or_attr_call():
142+
async with contextlib.asynccontextmanager(agen_fn)():
143+
...

0 commit comments

Comments
 (0)