Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Changelog

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

24.11.1
======
- :ref:`ASYNC100 <async100>` now ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group` as cancellation sources.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- :ref:`ASYNC100 <async100>` now ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group` as cancellation sources.
- :ref:`ASYNC100 <async100>` now ignores :func:`trio.open_nursery` and :func:`anyio.create_task_group` as cancellation sources,
because they are schedule points but not cancellation points.

This is a subtle enough concept that we should not assume users are familiar with it - I'd also call out this distinction in the docs for ASYNC100, and create glossary entries for both which are linked from "checkpoint" and link in turn to Trio docs (eg for cancel_shielded_checkpoint() and checkpoint_if_cancelled())

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, feel free to reword my attempt :)


24.10.2
=======
- :ref:`ASYNC102 <async102>` now also warns about ``await()`` inside ``__aexit__``.
Expand Down
2 changes: 1 addition & 1 deletion flake8_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@


# CalVer: YY.month.patch, e.g. first release of July 2022 == "22.7.1"
__version__ = "24.10.2"
__version__ = "24.11.1"


# taken from https://github.com/Zac-HD/shed
Expand Down
30 changes: 26 additions & 4 deletions flake8_async/visitors/visitor91x.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
flatten_preserving_comments,
fnmatch_qualified_name_cst,
func_has_decorator,
identifier_to_string,
iter_guaranteed_once_cst,
with_has_call,
)
Expand Down Expand Up @@ -491,12 +492,34 @@ def _is_exception_suppressing_context_manager(self, node: cst.With) -> bool:
is not None
)

def _checkpoint_with(self, node: cst.With):
"""Conditionally checkpoints entry/exit of With.

If the with only contains calls to open_nursery/create_task_group, it's a schedule
point but not a cancellation point, so we treat it as a checkpoint for async91x
but not for async100.
"""
if getattr(node, "asynchronous", None):
for item in node.items:
if not isinstance(item.item, cst.Call) or not isinstance(
item.item.func, (cst.Attribute, cst.Name)
):
self.checkpoint()
break

func = identifier_to_string(item.item.func)
assert func is not None
if func not in ("trio.open_nursery", "anyio.create_task_group"):
self.checkpoint()
break
else:
self.uncheckpointed_statements = set()

# Async context managers can reasonably checkpoint on either or both of entry and
# exit. Given that we can't tell which, we assume "both" to avoid raising a
# missing-checkpoint warning when there might in fact be one (i.e. a false alarm).
def visit_With_body(self, node: cst.With):
if getattr(node, "asynchronous", None):
self.checkpoint()
self._checkpoint_with(node)

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

if getattr(original_node, "asynchronous", None):
self.checkpoint()
self._checkpoint_with(original_node)
return updated_node

# error if no checkpoint since earlier yield or function entry
Expand Down
11 changes: 11 additions & 0 deletions tests/autofix_files/async100.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,14 @@ async def fn(timeout):
if condition():
return
await trio.sleep(1)


async def nursery_no_cancel_point():
# error: 9, "trio", "CancelScope"
async with anyio.create_task_group():
...


async def dont_crash_on_non_name_or_attr_call():
async with contextlib.asynccontextmanager(agen_fn)():
...
13 changes: 13 additions & 0 deletions tests/autofix_files/async100.py.diff
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,16 @@

with contextlib.suppress(Exception):
with open("blah") as file:
@@ x,9 x,9 @@


async def nursery_no_cancel_point():
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
- async with anyio.create_task_group():
- ...
+ # error: 9, "trio", "CancelScope"
+ async with anyio.create_task_group():
+ ...


async def dont_crash_on_non_name_or_attr_call():
10 changes: 10 additions & 0 deletions tests/autofix_files/async100_trio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# AUTOFIX
# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist
# ANYIO_NO_ERROR # anyio.open_nursery doesn't exist
import trio


async def nursery_no_cancel_point():
# error: 9, "trio", "CancelScope"
async with trio.open_nursery():
...
12 changes: 12 additions & 0 deletions tests/autofix_files/async100_trio.py.diff
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
+++
@@ x,6 x,6 @@


async def nursery_no_cancel_point():
- with trio.CancelScope(): # error: 9, "trio", "CancelScope"
- async with trio.open_nursery():
- ...
+ # error: 9, "trio", "CancelScope"
+ async with trio.open_nursery():
+ ...
11 changes: 11 additions & 0 deletions tests/eval_files/async100.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,14 @@ async def fn(timeout):
if condition():
return
await trio.sleep(1)


async def nursery_no_cancel_point():
with trio.CancelScope(): # error: 9, "trio", "CancelScope"
async with anyio.create_task_group():
...


async def dont_crash_on_non_name_or_attr_call():
async with contextlib.asynccontextmanager(agen_fn)():
...
10 changes: 10 additions & 0 deletions tests/eval_files/async100_trio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# AUTOFIX
# ASYNCIO_NO_ERROR # asyncio.open_nursery doesn't exist
# ANYIO_NO_ERROR # anyio.open_nursery doesn't exist
import trio


async def nursery_no_cancel_point():
with trio.CancelScope(): # error: 9, "trio", "CancelScope"
async with trio.open_nursery():
...
Loading