Skip to content

Commit ba56421

Browse files
committed
initial implementation of ASYNC123
1 parent ab022b1 commit ba56421

File tree

5 files changed

+230
-0
lines changed

5 files changed

+230
-0
lines changed

.pre-commit-config.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ repos:
4040
rev: v1.11.2
4141
hooks:
4242
- id: mypy
43+
# uses py311 syntax, mypy configured for py39
44+
exclude: tests/eval_files/async123.py
4345

4446
- repo: https://github.com/RobertCraigie/pyright-python
4547
rev: v1.1.384

flake8_async/visitors/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
visitor105,
3737
visitor111,
3838
visitor118,
39+
visitor123,
3940
visitor_utility,
4041
visitors,
4142
)

flake8_async/visitors/visitor123.py

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""foo."""
2+
3+
from __future__ import annotations
4+
5+
import ast
6+
from typing import TYPE_CHECKING, Any
7+
8+
from .flake8asyncvisitor import Flake8AsyncVisitor
9+
from .helpers import error_class
10+
11+
if TYPE_CHECKING:
12+
from collections.abc import Mapping
13+
14+
15+
@error_class
16+
class Visitor123(Flake8AsyncVisitor):
17+
error_codes: Mapping[str, str] = {
18+
"ASYNC123": (
19+
"Raising a child exception of an exception group loses"
20+
" context, cause, and/or traceback of the exception inside the group."
21+
)
22+
}
23+
24+
def __init__(self, *args: Any, **kwargs: Any):
25+
super().__init__(*args, **kwargs)
26+
self.try_star = False
27+
self.exception_group_names: set[str] = set()
28+
self.child_exception_list_names: set[str] = set()
29+
self.child_exception_names: set[str] = set()
30+
31+
def _is_exception_group(self, node: ast.expr) -> bool:
32+
return (
33+
(isinstance(node, ast.Name) and node.id in self.exception_group_names)
34+
or (
35+
# a child exception might be an ExceptionGroup
36+
self._is_child_exception(node)
37+
)
38+
or (
39+
isinstance(node, ast.Call)
40+
and isinstance(node.func, ast.Attribute)
41+
and self._is_exception_group(node.func.value)
42+
and node.func.attr in ("subgroup", "split")
43+
)
44+
)
45+
46+
def _is_exception_list(self, node: ast.expr | None) -> bool:
47+
return (
48+
isinstance(node, ast.Name) and node.id in self.child_exception_list_names
49+
) or (
50+
isinstance(node, ast.Attribute)
51+
and node.attr == "exceptions"
52+
and self._is_exception_group(node.value)
53+
)
54+
55+
def _is_child_exception(self, node: ast.expr | None) -> bool:
56+
return (
57+
isinstance(node, ast.Name) and node.id in self.child_exception_names
58+
) or (isinstance(node, ast.Subscript) and self._is_exception_list(node.value))
59+
60+
def visit_Raise(self, node: ast.Raise):
61+
if self._is_child_exception(node.exc):
62+
self.error(node)
63+
64+
def visit_ExceptHandler(self, node: ast.ExceptHandler):
65+
self.save_state(
66+
node,
67+
"exception_group_names",
68+
"child_exception_list_names",
69+
"child_exception_names",
70+
copy=True,
71+
)
72+
if node.name is None or (
73+
not self.try_star
74+
and (node.type is None or "ExceptionGroup" not in ast.unparse(node.type))
75+
):
76+
self.novisit = True
77+
return
78+
self.exception_group_names = {node.name}
79+
80+
# ast.TryStar added in py311
81+
def visit_TryStar(self, node: ast.TryStar): # type: ignore[name-defined]
82+
self.save_state(node, "try_star", copy=False)
83+
self.try_star = True
84+
85+
def visit_Assign(self, node: ast.Assign | ast.AnnAssign):
86+
if node.value is None or not self.exception_group_names:
87+
return
88+
targets = (node.target,) if isinstance(node, ast.AnnAssign) else node.targets
89+
if self._is_child_exception(node.value):
90+
for target in targets:
91+
if isinstance(target, ast.Name):
92+
self.child_exception_names.add(target.id)
93+
elif self._is_exception_list(node.value):
94+
if len(targets) == 1 and isinstance(targets[0], ast.Name):
95+
self.child_exception_list_names.add(targets[0].id)
96+
# unpacking tuples and Starred and shit. Not implemented
97+
elif self._is_exception_group(node.value):
98+
for target in targets:
99+
if isinstance(target, ast.Name):
100+
self.exception_group_names.add(target.id)
101+
elif isinstance(target, ast.Tuple):
102+
for t in target.elts:
103+
if isinstance(t, ast.Name):
104+
self.exception_group_names.add(t.id)
105+
106+
visit_AnnAssign = visit_Assign
107+
108+
def visit_For(self, node: ast.For):
109+
if self._is_exception_list(node.iter) and isinstance(node.target, ast.Name):
110+
self.child_exception_names.add(node.target.id)

tests/eval_files/async123.py

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import copy
2+
import sys
3+
from typing import Any
4+
5+
if sys.version_info < (3, 11):
6+
from exceptiongroup import BaseExceptionGroup, ExceptionGroup
7+
8+
9+
def condition() -> bool:
10+
return True
11+
12+
13+
def any_fun(arg: Exception) -> Exception:
14+
return arg
15+
16+
17+
try:
18+
...
19+
except ExceptionGroup as e:
20+
if condition():
21+
raise e.exceptions[0] # error: 8
22+
elif condition():
23+
raise copy.copy(e.exceptions[0]) # safe
24+
elif condition():
25+
raise copy.deepcopy(e.exceptions[0]) # safe
26+
else:
27+
raise any_fun(e.exceptions[0]) # safe
28+
try:
29+
...
30+
except BaseExceptionGroup as e:
31+
raise e.exceptions[0] # error: 4
32+
try:
33+
...
34+
except ExceptionGroup as e:
35+
my_e = e.exceptions[0]
36+
raise my_e # error: 4
37+
try:
38+
...
39+
except ExceptionGroup as e:
40+
excs = e.exceptions
41+
my_e = excs[0]
42+
raise my_e # error: 4
43+
try:
44+
...
45+
except ExceptionGroup as e:
46+
excs_2 = e.subgroup(bool)
47+
if excs_2:
48+
raise excs_2.exceptions[0] # error: 8
49+
try:
50+
...
51+
except ExceptionGroup as e:
52+
excs_1, excs_2 = e.split(bool)
53+
if excs_1:
54+
raise excs_1.exceptions[0] # error: 8
55+
if excs_2:
56+
raise excs_2.exceptions[0] # error: 8
57+
58+
try:
59+
...
60+
except ExceptionGroup as e:
61+
f = e
62+
raise f.exceptions[0] # error: 4
63+
try:
64+
...
65+
except ExceptionGroup as e:
66+
excs = e.exceptions
67+
excs2 = excs
68+
raise excs2[0] # error: 4
69+
try:
70+
...
71+
except ExceptionGroup as e:
72+
my_exc = e.exceptions[0]
73+
my_exc2 = my_exc
74+
raise my_exc2 # error: 4
75+
76+
try:
77+
...
78+
except* Exception as e:
79+
raise e.exceptions[0] # error: 4
80+
81+
try:
82+
...
83+
except ExceptionGroup as e:
84+
raise e.exceptions[0].exceptions[0] # error: 4
85+
try:
86+
...
87+
except ExceptionGroup as e:
88+
excs = e.exceptions
89+
for exc in excs:
90+
if ...:
91+
raise exc # error: 12
92+
raise
93+
try:
94+
...
95+
except ExceptionGroup as e:
96+
ff: ExceptionGroup[Exception] = e
97+
raise ff.exceptions[0] # error: 4
98+
try:
99+
...
100+
except ExceptionGroup as e:
101+
raise e.subgroup(bool).exceptions[0] # type: ignore # error: 4
102+
103+
# not implemented
104+
try:
105+
...
106+
except ExceptionGroup as e:
107+
a, *b = e.exceptions
108+
raise a
109+
110+
# not implemented
111+
try:
112+
...
113+
except ExceptionGroup as e:
114+
x: Any = object()
115+
x.y = e
116+
raise x.y.exceptions[0]

tests/test_flake8_async.py

+1
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def _parse_eval_file(
482482
# doesn't check for it
483483
"ASYNC121",
484484
"ASYNC122",
485+
"ASYNC123",
485486
"ASYNC300",
486487
"ASYNC912",
487488
}

0 commit comments

Comments
 (0)