8000 fix various issues with ASYNC102 (#289) · python-trio/flake8-async@4d0423a · GitHub
[go: up one dir, main page]

Skip to content

Commit 4d0423a

Browse files
authored
fix various issues with ASYNC102 (#289)
- ASYNC102 and ASYNC120 now - handles nested cancel scopes - detects internal cancel scopes of nurseries as a way to shield&deadline - no longer treats trio.open_nursery or anyio.create_task_group as cancellation sources - handles the `shield` parameter to trio.fail_after and friends
1 parent c6b4172 commit 4d0423a

File tree

7 files changed

+119
-25
lines changed

7 files changed

+119
-25
lines changed

docs/changelog.rst

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

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

7+
24.9.3
8+
======
9+
- :ref:`ASYNC102 <async102>` and :ref:`ASYNC120 <async120>`:
10+
- handles nested cancel scopes
11+
- detects internal cancel scopes of nurseries as a way to shield&deadline
12+
- no longer treats :func:`trio.open_nursery` or :func:`anyio.create_task_group` as cancellation sources
13+
- handles the `shield` parameter to :func:`trio.fail_after` and friends (added in trio 0.27)
14+
715
24.9.2
816
======
917
- Fix false alarm in :ref:`ASYNC113 <async113>` and :ref:`ASYNC121 <async121>` with sync functions nested inside an async function.

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.9.2"
41+
__version__ = "24.9.3"
4242

4343

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

flake8_async/visitors/visitor102.py

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,17 @@ def __init__(self, node: ast.Call, funcname: str, _):
4242

4343
if self.funcname == "CancelScope":
4444
self.has_timeout = False
45+
for kw in node.keywords:
46+
# note: sets to True even if timeout is explicitly set to inf
47+
if kw.arg == "deadline":
48+
self.has_timeout = True
49+
50+
# trio 0.27 adds shield parameter to all scope helpers
51+
if self.funcname in cancel_scope_names:
4552
for kw in node.keywords:
4653
# Only accepts constant values
4754
if kw.arg == "shield" and isinstance(kw.value, ast.Constant):
4855
self.shielded = kw.value.value
49-
# sets to True even if timeout is explicitly set to inf
50-
if kw.arg == "deadline":
51-
self.has_timeout = True
5256

5357
def __init__(self, *args: Any, **kwargs: Any):
5458
super().__init__(*args, **kwargs)
@@ -109,7 +113,12 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
109113

110114
# Check for a `with trio.<scope_creator>`
111115
for item in node.items:
112-
call = get_matching_call(item.context_expr, *cancel_scope_names)
116+
call = get_matching_call(
117+
item.context_expr,
118+
"open_nursery",
119+
"create_task_group",
120+
*cancel_scope_names,
121+
)
113122
if call is None:
114123
continue
115124

@@ -122,7 +131,18 @@ def visit_With(self, node: ast.With | ast.AsyncWith):
122131
break
123132

124133
def visit_AsyncWith(self, node: ast.AsyncWith):
125-
self.async_call_checker(node)
134+
# trio.open_nursery and anyio.create_task_group are not cancellation points
135+
# so only treat this as an async call if it contains a call that does not match.
136+
# asyncio.TaskGroup() appears to be a source of cancellation when exiting.
137+
for item in node.items:
138+
if not (
139+
get_matching_call(item.context_expr, "open_nursery", base="trio")
140+
or get_matching_call(
141+
item.context_expr, "create_task_group", base="anyio"
142+
)
143+
):
144+
self.async_call_checker(node)
145+
break
126146
self.visit_With(node)
127147

128148
def visit_Try(self, node: ast.Try):
@@ -160,18 +180,31 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler):
160180 9E88

161181
def visit_Assign(self, node: ast.Assign):
162182
# checks for <scopename>.shield = [True/False]
183+
# and <scopename>.cancel_scope.shield
184+
# We don't care to differentiate between them depending on if the scope is
185+
# a nursery or not, so e.g. `cs.cancel_scope.shield`/`nursery.shield` will "work"
163186
if self._trio_context_managers and len(node.targets) == 1:
164-
last_scope = self._trio_context_managers[-1]
165187
target = node.targets[0]
166-
if (
167-
last_scope.variable_name is not None
168-
and isinstance(target, ast.Attribute)
169-
and isinstance(target.value, ast.Name)
170-
and target.value.id == last_scope.variable_name
171-
and target.attr == "shield"
172-
and isinstance(node.value, ast.Constant)
173-
):
174-
last_scope.shielded = node.value.value
188+
for scope in reversed(self._trio_context_managers):
189+
if (
190+
scope.variable_name is not None
191+
and isinstance(node.value, ast.Constant)
192+
and isinstance(target, ast.Attribute)
193+
and target.attr == "shield"
194+
and (
195+
(
196+
isinstance(target.value, ast.Name)
197+
and target.value.id == scope.variable_name
198+
)
199+
or (
200+
isinstance(target.value, ast.Attribute)
201+
and target.value.attr == "cancel_scope"
202+
and isinstance(target.value.value, ast.Name)
203+
and target.value.value.id == scope.variable_name
204+
)
205+
)
206+
):
207+
scope.shielded = node.value.value
175208

176209
def visit_FunctionDef(
177210
self, node: ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda

tests/eval_files/async102.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ async def foo():
2121
s.shield = True
2222
await foo()
2323

24+
try:
25+
pass
26+
finally:
27+
with trio.move_on_after(30, shield=True) as s:
28+
await foo()
29+
2430
try:
2531
pass
2632
finally:
@@ -116,15 +122,6 @@ async def foo():
116122
await foo() # error: 12, Statement("try/finally", lineno-7)
117123
try:
118124
pass
119-
finally:
120-
# false alarm, open_nursery does not block/checkpoint on entry.
121-
async with trio.open_nursery() as nursery: # error: 8, Statement("try/finally", lineno-4)
122-
nursery.cancel_scope.deadline = trio.current_time() + 10
123-
nursery.cancel_scope.shield = True
124-
# false alarm, we currently don't handle nursery.cancel_scope.[deadline/shield]
125-
await foo() # error: 12, Statement("try/finally", lineno-8)
126-
try:
127-
pass
128125
finally:
129126
with trio.CancelScope(deadline=30, shield=True):
130127
with trio.move_on_after(30):
@@ -286,3 +283,24 @@ async def foo_nested_funcdef():
286283

287284
async def foobar():
288285
await foo()
286+
287+
288+
# nested cs
289+
async def foo_nested_cs():
290+
try:
291+
...
292+
except:
293+
with trio.CancelScope(deadline=10) as cs1:
294+
with trio.CancelScope(deadline=10) as cs2:
295+
await foo() # error: 16, Statement("bare except", lineno-3)
296+
cs1.shield = True
297+
await foo()
298+
cs1.shield = False
299+
await foo() # error: 16, Statement("bare except", lineno-7)
300+
cs2.shield = True
301+
await foo()
302+
await foo() # error: 12, Statement("bare except", lineno-10)
303+
cs2.shield = True
304+
await foo() # error: 12, Statement("bare except", lineno-12)
305+
cs1.shield = True
306+
await foo()

tests/eval_files/async102_anyio.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,16 @@ async def foo_anyio_shielded():
7272
await foo() # safe
7373
except BaseException:
7474
await foo() # safe
75+
76+
77+
# anyio.create_task_group is not a source of cancellations
78+
async def foo_open_nursery_no_cancel():
79+
try:
80+
pass
81+
finally:
82+
# create_task_group does not block/checkpoint on entry, and is not
83+
# a cancellation point on exit.
84+
async with anyio.create_task_group() as tg:
85+
tg.cancel_scope.deadline = anyio.current_time() + 10
86+
tg.cancel_scope.shield = True
87+
await foo()

tests/eval_files/async102_asyncio.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,12 @@ async def foo():
3737
await asyncio.shield( # error: 8, Statement("try/finally", lineno-3)
3838
asyncio.wait_for(foo())
3939
)
40+
41+
42+
# asyncio.TaskGroup *is* a source of cancellations (on exit)
43+
async def foo_open_nursery_no_cancel():
44+
try:
45+
pass
46+
finally:
47+
async with asyncio.TaskGroup() as tg: # error: 8, Statement("try/finally", lineno-3)
48+
...

tests/eval_files/async102_trio.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ async def foo5():
3131
await foo() # safe
3232
except BaseException:
3333
await foo() # safe, since after trio.Cancelled
34+
35+
36+
# trio.open_nursery is not a source of cancellations
37+
async def foo_open_nursery_no_cancel():
38+
try:
39+
pass
40+
finally:
41+
# open_nursery does not block/checkpoint on entry, and is not
42+
# a cancellation point on exit.
43+
async with trio.open_nursery() as nursery:
44+
nursery.cancel_scope.deadline = trio.current_time() + 10
45+
nursery.cancel_scope.shield = True
46+
await foo()

0 commit comments

Comments
 (0)
0