Skip to content

Commit ac7a9c1

Browse files
authored
add except* support to 102, 103, 104, 120, 910, 911 & 912 (#358)
* add except* support to 102, 103, 104, 120, 910, 911 & 912
1 parent 895c550 commit ac7a9c1

14 files changed

+349
-15
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ repos:
4242
hooks:
4343
- id: mypy
4444
# uses py311 syntax, mypy configured for py39
45-
exclude: tests/eval_files/.*_py311.py
45+
exclude: tests/(eval|autofix)_files/.*_py311.py
4646

4747
- repo: https://github.com/RobertCraigie/pyright-python
4848
rev: v1.1.396

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.3.1
8+
======
9+
- Add except* support to ASYNC102, 103, 104, 120, 910, 911, 912.
10+
711
25.2.3
812
=======
913
- No longer require ``flake8`` for installation... so if you require support for config files you must install ``flake8-async[flake8]``

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.3
36+
rev: 25.3.1
3737
hooks:
3838
- id: flake8-async
3939
# args: ["--enable=ASYNC100,ASYNC112", "--disable=", "--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.3"
41+
__version__ = "25.3.1"
4242

4343

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

flake8_async/visitors/visitor102_120.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def visit_AsyncWith(self, node: ast.AsyncWith):
149149
break
150150
self.visit_With(node)
151151

152-
def visit_Try(self, node: ast.Try):
152+
def visit_Try(self, node: ast.Try | ast.TryStar): # type: ignore[name-defined]
153153
self.save_state(
154154
node, "_critical_scope", "_trio_context_managers", "cancelled_caught"
155155
)
@@ -165,6 +165,8 @@ def visit_Try(self, node: ast.Try):
165165
self._critical_scope = Statement("try/finally", node.lineno, node.col_offset)
166166
self.visit_nodes(node.finalbody)
167167

168+
visit_TryStar = visit_Try
169+
168170
def visit_ExceptHandler(self, node: ast.ExceptHandler):
169171
# if we're inside a critical scope, a nested except should never override that
170172
if self._critical_scope is not None and self._critical_scope.name != "except":

flake8_async/visitors/visitor103_104.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ def visit_Return(self, node: ast.Return | ast.Yield):
160160
visit_Yield = visit_Return
161161

162162
# Treat Try's as fully covering only if `finally` always raises.
163-
def visit_Try(self, node: ast.Try):
163+
def visit_Try(self, node: ast.Try | ast.TryStar): # type: ignore[name-defined]
164164
self.save_state(node, "cancelled_caught", copy=True)
165165
self.cancelled_caught = set()
166166

@@ -179,6 +179,8 @@ def visit_Try(self, node: ast.Try):
179179
# but it's fine if we raise in finally
180180
self.visit_nodes(node.finalbody)
181181

182+
visit_TryStar = visit_Try
183+
182184
# Treat if's as fully covering if both `if` and `else` raise.
183185
# `elif` is parsed by the ast as a new if statement inside the else.
184186
def visit_If(self, node: ast.If):

flake8_async/visitors/visitor91x.py

+24-10
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ def leave_Yield(
768768
# try can jump into any except or into the finally* at any point during it's
769769
# execution so we need to make sure except & finally can handle worst-case
770770
# * unless there's a bare except / except BaseException - not implemented.
771-
def visit_Try(self, node: cst.Try):
771+
def visit_Try(self, node: cst.Try | cst.TryStar):
772772
if not self.async_function:
773773
return
774774
self.save_state(node, "try_state", copy=True)
@@ -784,39 +784,41 @@ def visit_Try(self, node: cst.Try):
784784
Statement("yield", pos.line, pos.column) # type: ignore
785785
)
786786

787-
def leave_Try_body(self, node: cst.Try):
787+
def leave_Try_body(self, node: cst.Try | cst.TryStar):
788788
# save state at end of try for entering else
789789
self.try_state.try_checkpoint = self.uncheckpointed_statements
790790

791791
# check that all except handlers checkpoint (await or most likely raise)
792792
self.try_state.except_uncheckpointed_statements = set()
793793

794-
def visit_ExceptHandler(self, node: cst.ExceptHandler):
794+
def visit_ExceptHandler(self, node: cst.ExceptHandler | cst.ExceptStarHandler):
795795
# enter with worst case of try
796796
self.uncheckpointed_statements = (
797797
self.try_state.body_uncheckpointed_statements.copy()
798798
)
799799

800800
def leave_ExceptHandler(
801-
self, original_node: cst.ExceptHandler, updated_node: cst.ExceptHandler
802-
) -> cst.ExceptHandler:
801+
self,
802+
original_node: cst.ExceptHandler | cst.ExceptStarHandler,
803+
updated_node: cst.ExceptHandler | cst.ExceptStarHandler,
804+
) -> Any: # not worth creating a TypeVar to handle correctly
803805
self.try_state.except_uncheckpointed_statements.update(
804806
self.uncheckpointed_statements
805807
)
806808
return updated_node
807809

808-
def visit_Try_orelse(self, node: cst.Try):
810+
def visit_Try_orelse(self, node: cst.Try | cst.TryStar):
809811
# check else
810812
# if else runs it's after all of try, so restore state to back then
811813
self.uncheckpointed_statements = self.try_state.try_checkpoint
812814

813-
def leave_Try_orelse(self, node: cst.Try):
815+
def leave_Try_orelse(self, node: cst.Try | cst.TryStar):
814816
# checkpoint if else checkpoints, and all excepts checkpoint
815817
self.uncheckpointed_statements.update(
816818
self.try_state.except_uncheckpointed_statements
817819
)
818820

819-
def visit_Try_finalbody(self, node: cst.Try):
821+
def visit_Try_finalbody(self, node: cst.Try | cst.TryStar):
820822
if node.finalbody:
821823
self.try_state.added = (
822824
self.try_state.body_uncheckpointed_statements.difference(
@@ -835,14 +837,26 @@ def visit_Try_finalbody(self, node: cst.Try):
835837
):
836838
self.uncheckpointed_statements.update(self.try_state.added)
837839

838-
def leave_Try_finalbody(self, node: cst.Try):
840+
def leave_Try_finalbody(self, node: cst.Try | cst.TryStar):
839841
if node.finalbody:
840842
self.uncheckpointed_statements.difference_update(self.try_state.added)
841843

842-
def leave_Try(self, original_node: cst.Try, updated_node: cst.Try) -> cst.Try:
844+
def leave_Try(
845+
self, original_node: cst.Try | cst.TryStar, updated_node: cst.Try | cst.TryStar
846+
) -> cst.Try | cst.TryStar:
843847
self.restore_state(original_node)
844848
return updated_node
845849

850+
visit_TryStar = visit_Try
851+
leave_TryStar = leave_Try
852+
leave_TryStar_body = leave_Try_body
853+
visit_TryStar_orelse = visit_Try_orelse
854+
leave_TryStar_orelse = leave_Try_orelse
855+
visit_TryStar_finalbody = visit_Try_finalbody
856+
leave_TryStar_finalbody = leave_Try_finalbody
857+
visit_ExceptStarHandler = visit_ExceptHandler
858+
leave_ExceptStarHandler = leave_ExceptHandler
859+
846860
def leave_If_test(self, node: cst.If | cst.IfExp) -> None:
847861
if not self.async_function:
848862
return

tests/autofix_files/async91x_py311.py

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Test for ASYNC91x rules with except* blocks.
2+
3+
ASYNC910: async-function-no-checkpoint
4+
ASYNC911: async-generator-no-checkpoint
5+
ASYNC913: indefinite-loop-no-guaranteed-checkpoint
6+
7+
async912 handled in separate file
8+
"""
9+
10+
# ARG --enable=ASYNC910,ASYNC911,ASYNC913
11+
# AUTOFIX
12+
# ASYNCIO_NO_AUTOFIX
13+
import trio
14+
15+
16+
async def foo(): ...
17+
18+
19+
async def foo_try_except_star_1(): # ASYNC910: 0, "exit", Statement("function definition", lineno)
20+
try:
21+
await foo()
22+
except* ValueError:
23+
...
24+
except* RuntimeError:
25+
raise
26+
else:
27+
await foo()
28+
await trio.lowlevel.checkpoint()
29+
30+
31+
async def foo_try_except_star_2(): # safe
32+
try:
33+
...
34+
except* ValueError:
35+
...
36+
finally:
37+
await foo()
38+
39+
40+
async def foo_try_except_star_3(): # safe
41+
try:
42+
await foo()
43+
except* ValueError:
44+
raise
45+
46+
47+
# Multiple except* handlers - should all guarantee checkpoint/raise
48+
async def foo_try_except_star_4():
49+
try:
50+
await foo()
51+
except* ValueError:
52+
await foo()
53+
except* TypeError:
54+
raise
55+
except* Exception:
56+
raise
57+
58+
59+
async def try_else_no_raise_in_except(): # ASYNC910: 0, "exit", Statement("function definition", lineno)
60+
try:
61+
...
62+
except* ValueError:
63+
...
64+
else:
65+
await foo()
66+
await trio.lowlevel.checkpoint()
67+
68+
69+
async def try_else_raise_in_except():
70+
try:
71+
...
72+
except* ValueError:
73+
raise
74+
else:
75+
await foo()
76+
77+
78+
async def check_async911(): # ASYNC911: 0, "exit", Statement("yield", lineno+7)
79+
try:
80+
await foo()
81+
except* ValueError:
82+
...
83+
except* RuntimeError:
84+
raise
85+
await trio.lowlevel.checkpoint()
86+
yield # ASYNC911: 4, "yield", Statement("function definition", lineno-7)
87+
await trio.lowlevel.checkpoint()
88+
89+
90+
async def check_async913():
91+
while True: # ASYNC913: 4
92+
await trio.lowlevel.checkpoint()
93+
try:
94+
await foo()
95+
except* ValueError:
96+
# Missing checkpoint
97+
...
+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
+++
3+
@@ x,6 x,7 @@
4+
raise
5+
else:
6+
await foo()
7+
+ await trio.lowlevel.checkpoint()
8+
9+
10+
async def foo_try_except_star_2(): # safe
11+
@@ x,6 x,7 @@
12+
...
13+
else:
14+
await foo()
15+
+ await trio.lowlevel.checkpoint()
16+
17+
18+
async def try_else_raise_in_except():
19+
@@ x,11 x,14 @@
20+
...
21+
except* RuntimeError:
22+
raise
23+
+ await trio.lowlevel.checkpoint()
24+
yield # ASYNC911: 4, "yield", Statement("function definition", lineno-7)
25+
+ await trio.lowlevel.checkpoint()
26+
27+
28+
async def check_async913():
29+
while True: # ASYNC913: 4
30+
+ await trio.lowlevel.checkpoint()
31+
try:
32+
await foo()
33+
except* ValueError:
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Test for ASYNC102/ASYNC120 with except*
2+
3+
ASYNC102: await-in-finally-or-cancelled
4+
5+
ASYNC120: await-in-except
6+
"""
7+
8+
# type: ignore
9+
# ARG --enable=ASYNC102,ASYNC120
10+
# NOASYNCIO # TODO: support asyncio shields
11+
import trio
12+
13+
14+
async def foo():
15+
try:
16+
...
17+
except* ValueError:
18+
await foo() # ASYNC120: 8, Statement("except", lineno-1)
19+
raise
20+
except* BaseException:
21+
await foo() # ASYNC102: 8, Statement("BaseException", lineno-1)
22+
finally:
23+
await foo() # ASYNC102: 8, Statement("try/finally", lineno-8)
24+
25+
try:
26+
...
27+
except* BaseException:
28+
with trio.move_on_after(30, shield=True):
29+
await foo()
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test for ASYNC103/ASYNC104 with except* blocks.
2+
3+
ASYNC103: no-reraise-cancelled
4+
ASYNC104: cancelled-not-raised
5+
"""
6+
7+
# ARG --enable=ASYNC103,ASYNC104
8+
9+
try:
10+
...
11+
except* BaseException: # ASYNC103_trio: 8, "BaseException"
12+
...
13+
14+
try:
15+
...
16+
except* BaseException:
17+
raise
18+
19+
try:
20+
...
21+
except* ValueError:
22+
...
23+
except* BaseException: # ASYNC103_trio: 8, "BaseException"
24+
...
25+
26+
try:
27+
...
28+
except* BaseException:
29+
raise ValueError # ASYNC104: 4
30+
31+
32+
def foo():
33+
try:
34+
...
35+
except* BaseException: # ASYNC103_trio: 12, "BaseException"
36+
return # ASYNC104: 8
37+
try:
38+
...
39+
except* BaseException:
40+
raise ValueError # ASYNC104: 8

tests/eval_files/async912_py311.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# ASYNC912 can't be tested with the other 91x rules since there's no universal
2+
# cancelscope name across trio/asyncio/anyio - so we need ASYNCIO_NO_ERROR
3+
4+
5+
# ASYNCIO_NO_ERROR
6+
async def foo(): ...
7+
8+
9+
async def check_async912():
10+
with trio.move_on_after(30): # ASYNC912: 9
11+
try:
12+
await foo()
13+
except* ValueError:
14+
# Missing checkpoint
15+
...
16+
await foo()

0 commit comments

Comments
 (0)