Skip to content

Commit b960272

Browse files
authored
add except* support to B012&B025 (#500)
* add except* support to B012&B025, add tests for any except-handling rules * now prints except* in error messages
1 parent 4fed293 commit b960272

File tree

11 files changed

+535
-42
lines changed

11 files changed

+535
-42
lines changed

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ Change Log
364364
----------
365365

366366

367+
FUTURE
368+
~~~~~~
369+
370+
* B012 and B025 now also handle try/except*
371+
367372
24.10.31
368373
~~~~~~~~
369374

bugbear.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def _flatten_excepthandler(node: ast.expr | None) -> Iterator[ast.expr | None]:
243243
yield expr
244244

245245

246-
def _check_redundant_excepthandlers(names: Sequence[str], node):
246+
def _check_redundant_excepthandlers(names: Sequence[str], node, in_trystar):
247247
# See if any of the given exception names could be removed, e.g. from:
248248
# (MyError, MyError) # duplicate names
249249
# (MyError, BaseException) # everything derives from the Base
@@ -275,7 +275,7 @@ def _check_redundant_excepthandlers(names: Sequence[str], node):
275275
return B014(
276276
node.lineno,
277277
node.col_offset,
278-
vars=(", ".join(names), as_, desc),
278+
vars=(", ".join(names), as_, desc, in_trystar),
279279
)
280280
return None
281281

@@ -388,6 +388,9 @@ class BugBearVisitor(ast.NodeVisitor):
388388
_b023_seen: set[error] = attr.ib(factory=set, init=False)
389389
_b005_imports: set[str] = attr.ib(factory=set, init=False)
390390

391+
# set to "*" when inside a try/except*, for correctly printing errors
392+
in_trystar: str = attr.ib(default="")
393+
391394
if False:
392395
# Useful for tracing what the hell is going on.
393396

@@ -457,7 +460,7 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
457460
else:
458461
self.b040_caught_exception = B040CaughtException(node.name, False)
459462

460-
names = self.check_for_b013_b029_b030(node)
463+
names = self.check_for_b013_b014_b029_b030(node)
461464

462465
if (
463466
"BaseException" in names
@@ -605,6 +608,12 @@ def visit_Try(self, node) -> None:
605608
self.check_for_b025(node)
606609
self.generic_visit(node)
607610

611+
def visit_TryStar(self, node) -> None:
612+
outer_trystar = self.in_trystar
613+
self.in_trystar = "*"
614+
self.visit_Try(node)
615+
self.in_trystar = outer_trystar
616+
608617
def visit_Compare(self, node) -> None:
609618
self.check_for_b015(node)
610619
self.generic_visit(node)
@@ -763,15 +772,17 @@ def _loop(node, bad_node_types) -> None:
763772
bad_node_types = (ast.Return,)
764773

765774
elif isinstance(node, bad_node_types):
766-
self.errors.append(B012(node.lineno, node.col_offset))
775+
self.errors.append(
776+
B012(node.lineno, node.col_offset, vars=(self.in_trystar,))
777+
)
767778

768779
for child in ast.iter_child_nodes(node):
769780
_loop(child, bad_node_types)
770781

771782
for child in node.finalbody:
772783
_loop(child, (ast.Return, ast.Continue, ast.Break))
773784

774-
def check_for_b013_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
785+
def check_for_b013_b014_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
775786
handlers: Iterable[ast.expr | None] = _flatten_excepthandler(node.type)
776787
names: list[str] = []
777788
bad_handlers: list[object] = []
@@ -791,16 +802,27 @@ def check_for_b013_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
791802
if bad_handlers:
792803
self.errors.append(B030(node.lineno, node.col_offset))
793804
if len(names) == 0 and not bad_handlers and not ignored_handlers:
794-
self.errors.append(B029(node.lineno, node.col_offset))
805+
self.errors.append(
806+
B029(node.lineno, node.col_offset, vars=(self.in_trystar,))
807+
)
795808
elif (
796809
len(names) == 1
797810
and not bad_handlers
798811
and not ignored_handlers
799812
and isinstance(node.type, ast.Tuple)
800813
):
801-
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
814+
self.errors.append(
815+
B013(
816+
node.lineno,
817+
node.col_offset,
818+
vars=(
819+
*names,
820+
self.in_trystar,
821+
),
822+
)
823+
)
802824
else:
803-
maybe_error = _check_redundant_excepthandlers(names, node)
825+
maybe_error = _check_redundant_excepthandlers(names, node, self.in_trystar)
804826
if maybe_error is not None:
805827
self.errors.append(maybe_error)
806828
return names
@@ -1216,7 +1238,9 @@ def check_for_b904(self, node) -> None:
12161238
and not (isinstance(node.exc, ast.Name) and node.exc.id.islower())
12171239
and any(isinstance(n, ast.ExceptHandler) for n in self.node_stack)
12181240
):
1219-
self.errors.append(B904(node.lineno, node.col_offset))
1241+
self.errors.append(
1242+
B904(node.lineno, node.col_offset, vars=(self.in_trystar,))
1243+
)
12201244

12211245
def walk_function_body(self, node):
12221246
def _loop(parent, node):
@@ -1480,7 +1504,9 @@ def check_for_b025(self, node) -> None:
14801504
# sort to have a deterministic output
14811505
duplicates = sorted({x for x in seen if seen.count(x) > 1})
14821506
for duplicate in duplicates:
1483-
self.errors.append(B025(node.lineno, node.col_offset, vars=(duplicate,)))
1507+
self.errors.append(
1508+
B025(node.lineno, node.col_offset, vars=(duplicate, self.in_trystar))
1509+
)
14841510

14851511
@staticmethod
14861512
def _is_infinite_iterator(node: ast.expr) -> bool:
@@ -2073,6 +2099,7 @@ def visit_Lambda(self, node) -> None:
20732099
error = namedtuple("error", "lineno col message type vars")
20742100
Error = partial(partial, error, type=BugBearChecker, vars=())
20752101

2102+
# note: bare except* is a syntax error, so B001 does not need to handle it
20762103
B001 = Error(
20772104
message=(
20782105
"B001 Do not use bare `except:`, it also catches unexpected "
@@ -2194,20 +2221,20 @@ def visit_Lambda(self, node) -> None:
21942221
B012 = Error(
21952222
message=(
21962223
"B012 return/continue/break inside finally blocks cause exceptions "
2197-
"to be silenced. Exceptions should be silenced in except blocks. Control "
2224+
"to be silenced. Exceptions should be silenced in except{0} blocks. Control "
21982225
"statements can be moved outside the finally block."
21992226
)
22002227
)
22012228
B013 = Error(
22022229
message=(
22032230
"B013 A length-one tuple literal is redundant. "
2204-
"Write `except {0}:` instead of `except ({0},):`."
2231+
"Write `except{1} {0}:` instead of `except{1} ({0},):`."
22052232
)
22062233
)
22072234
B014 = Error(
22082235
message=(
2209-
"B014 Redundant exception types in `except ({0}){1}:`. "
2210-
"Write `except {2}{1}:`, which catches exactly the same exceptions."
2236+
"B014 Redundant exception types in `except{3} ({0}){1}:`. "
2237+
"Write `except{3} {2}{1}:`, which catches exactly the same exceptions."
22112238
)
22122239
)
22132240
B014_REDUNDANT_EXCEPTIONS = {
@@ -2296,8 +2323,8 @@ def visit_Lambda(self, node) -> None:
22962323
)
22972324
B025 = Error(
22982325
message=(
2299-
"B025 Exception `{0}` has been caught multiple times. Only the first except"
2300-
" will be considered and all other except catches can be safely removed."
2326+
"B025 Exception `{0}` has been caught multiple times. Only the first except{0}"
2327+
" will be considered and all other except{0} catches can be safely removed."
23012328
)
23022329
)
23032330
B026 = Error(
@@ -2325,7 +2352,7 @@ def visit_Lambda(self, node) -> None:
23252352
)
23262353
B029 = Error(
23272354
message=(
2328-
"B029 Using `except ():` with an empty tuple does not handle/catch "
2355+
"B029 Using `except{0} ():` with an empty tuple does not handle/catch "
23292356
"anything. Add exceptions to handle."
23302357
)
23312358
)
@@ -2414,7 +2441,7 @@ def visit_Lambda(self, node) -> None:
24142441

24152442
B904 = Error(
24162443
message=(
2417-
"B904 Within an `except` clause, raise exceptions with `raise ... from err` or"
2444+
"B904 Within an `except{0}` clause, raise exceptions with `raise ... from err` or"
24182445
" `raise ... from None` to distinguish them from errors in exception handling. "
24192446
" See https://docs.python.org/3/tutorial/errors.html#exception-chaining for"
24202447
" details."

tests/b012_py311.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
def a():
2+
try:
3+
pass
4+
except* Exception:
5+
pass
6+
finally:
7+
return # warning
8+
9+
10+
def b():
11+
try:
12+
pass
13+
except* Exception:
14+
pass
15+
finally:
16+
if 1 + 0 == 2 - 1:
17+
return # warning
18+
19+
20+
def c():
21+
try:
22+
pass
23+
except* Exception:
24+
pass
25+
finally:
26+
try:
27+
return # warning
28+
except* Exception:
29+
pass

tests/b013_py311.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""
2+
Should emit:
3+
B013 - on lines 10 and 28
4+
"""
5+
6+
import re
7+
8+
try:
9+
pass
10+
except* (ValueError,):
11+
# pointless use of tuple
12+
pass
13+
14+
# fmt: off
15+
# Turn off black to keep brackets around
16+
# single except*ion for testing purposes.
17+
try:
18+
pass
19+
except* (ValueError):
20+
# not using a tuple means it's OK (if odd)
21+
pass
22+
# fmt: on
23+
24+
try:
25+
pass
26+
except* ValueError:
27+
# no warning here, all good
28+
pass
29+
30+
try:
31+
pass
32+
except* (re.error,):
33+
# pointless use of tuple with dotted attribute
34+
pass
35+
36+
try:
37+
pass
38+
except* (a.b.c.d, b.c.d):
39+
# attribute of attribute, etc.
40+
pass

tests/b014_py311.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""
2+
This is a copy of b014 but with except* instead. Should emit:
3+
B014 - on lines 11, 17, 28, 42, 49, 56, and 74.
4+
"""
5+
6+
import binascii
7+
import re
8+
9+
try:
10+
pass
11+
except* (Exception, TypeError):
12+
# TypeError is a subclass of Exception, so it doesn't add anything
13+
pass
14+
15+
try:
16+
pass
17+
except* (OSError, OSError) as err:
18+
# Duplicate exception types are useless
19+
pass
20+
21+
22+
class MyError(Exception):
23+
pass
24+
25+
26+
try:
27+
pass
28+
except* (MyError, MyError):
29+
# Detect duplicate non-builtin errors
30+
pass
31+
32+
33+
try:
34+
pass
35+
except* (MyError, Exception) as e:
36+
# Don't assume that we're all subclasses of Exception
37+
pass
38+
39+
40+
try:
41+
pass
42+
except* (MyError, BaseException) as e:
43+
# But we *can* assume that everything is a subclass of BaseException
44+
raise e
45+
46+
47+
try:
48+
pass
49+
except* (re.error, re.error):
50+
# Duplicate exception types as attributes
51+
pass
52+
53+
54+
try:
55+
pass
56+
except* (IOError, EnvironmentError, OSError):
57+
# Detect if a primary exception and any its aliases are present.
58+
#
59+
# Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
60+
# socket.error and select.error are aliases of OSError. See PEP 3151 for
61+
# more info.
62+
pass
63+
64+
65+
try:
66+
pass
67+
except* (MyException, NotImplemented):
68+
# NotImplemented is not an exception, let's not crash on it.
69+
pass
70+
71+
72+
try:
73+
pass
74+
except* (ValueError, binascii.Error):
75+
# binascii.Error is a subclass of ValueError.
76+
pass

tests/b025_py311.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""
2+
Should emit:
3+
B025 - on lines 15, 22, 31
4+
"""
5+
6+
import pickle
7+
8+
try:
9+
a = 1
10+
except* ValueError:
11+
a = 2
12+
finally:
13+
a = 3
14+
15+
try:
16+
a = 1
17+
except* ValueError:
18+
a = 2
19+
except* ValueError:
20+
a = 2
21+
22+
try:
23+
a = 1
24+
except* pickle.PickleError:
25+
a = 2
26+
except* ValueError:
27+
a = 2
28+
except* pickle.PickleError:
29+
a = 2
30+
31+
try:
32+
a = 1
33+
except* (ValueError, TypeError):
34+
a = 2
35+
except* ValueError:
36+
a = 2
37+
except* (OSError, TypeError):
38+
a = 2

tests/b029_py311.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""
2+
Should emit:
3+
B029 - on lines 8 and 13
4+
"""
5+
6+
try:
7+
pass
8+
except* ():
9+
pass
10+
11+
try:
12+
pass
13+
except* () as e:
14+
pass

0 commit comments

Comments
 (0)