Skip to content

Commit 5314dd8

Browse files
committed
add except* support to B012&B025, add tests for any except-handling rules
1 parent 108bba4 commit 5314dd8

File tree

10 files changed

+419
-2
lines changed

10 files changed

+419
-2
lines changed

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ Change Log
360360
----------
361361

362362

363+
FUTURE
364+
~~~~~~
365+
366+
* B012 and B025 now also handle try/except*
367+
363368
24.10.31
364369
~~~~~~~~
365370

bugbear.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
457457
else:
458458
self.b040_caught_exception = B040CaughtException(node.name, False)
459459

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

462462
if (
463463
"BaseException" in names
@@ -604,6 +604,8 @@ def visit_Try(self, node) -> None:
604604
self.check_for_b025(node)
605605
self.generic_visit(node)
606606

607+
visit_TryStar = visit_Try
608+
607609
def visit_Compare(self, node) -> None:
608610
self.check_for_b015(node)
609611
self.generic_visit(node)
@@ -770,7 +772,7 @@ def _loop(node, bad_node_types) -> None:
770772
for child in node.finalbody:
771773
_loop(child, (ast.Return, ast.Continue, ast.Break))
772774

773-
def check_for_b013_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
775+
def check_for_b013_b014_b029_b030(self, node: ast.ExceptHandler) -> list[str]:
774776
handlers: Iterable[ast.expr | None] = _flatten_excepthandler(node.type)
775777
names: list[str] = []
776778
bad_handlers: list[object] = []
@@ -2058,6 +2060,7 @@ def visit_Lambda(self, node) -> None:
20582060
error = namedtuple("error", "lineno col message type vars")
20592061
Error = partial(partial, error, type=BugBearChecker, vars=())
20602062

2063+
# note: bare except* is a syntax error
20612064
B001 = Error(
20622065
message=(
20632066
"B001 Do not use bare `except:`, it also catches unexpected "

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/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

tests/b030_py311.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
try:
2+
pass
3+
except* (ValueError, (RuntimeError, (KeyError, TypeError))): # error
4+
pass
5+
6+
try:
7+
pass
8+
except* (ValueError, *(RuntimeError, *(KeyError, TypeError))): # ok
9+
pass
10+
11+
try:
12+
pass
13+
except* 1: # error
14+
pass
15+
16+
try:
17+
pass
18+
except* (1, ValueError): # error
19+
pass
20+
21+
try:
22+
pass
23+
except* (ValueError, *(RuntimeError, TypeError)): # ok
24+
pass
25+
26+
27+
def what_to_catch():
28+
return (ValueError, TypeError)
29+
30+
31+
try:
32+
pass
33+
except* what_to_catch(): # ok
34+
pass
35+
36+
37+
try:
38+
pass
39+
except* a.b[1].c: # ok
40+
pass

tests/b036_py311.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
try:
3+
pass
4+
except* BaseException: # bad
5+
print("aaa")
6+
pass
7+
8+
9+
try:
10+
pass
11+
except* BaseException as ex: # bad
12+
print(ex)
13+
pass
14+
15+
16+
try:
17+
pass
18+
except* ValueError:
19+
raise
20+
except* BaseException: # bad
21+
pass
22+
23+
24+
try:
25+
pass
26+
except* BaseException: # ok - reraised
27+
print("aaa")
28+
raise
29+
30+
31+
try:
32+
pass
33+
except* BaseException as ex: # bad - raised something else
34+
print("aaa")
35+
raise KeyError from ex
36+
37+
try:
38+
pass
39+
except* BaseException as e:
40+
raise e # ok - raising same thing
41+
42+
try:
43+
pass
44+
except* BaseException:
45+
if 0:
46+
raise # ok - raised somewhere within branch
47+
48+
try:
49+
pass
50+
except* BaseException:
51+
try: # nested try
52+
pass
53+
except* ValueError:
54+
raise # bad - raising within a nested try/except*, but not in the main one
55+
56+
try:
57+
pass
58+
except* BaseException:
59+
raise a.b from None # bad (regression test for #449)

tests/b904_py311.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Should emit:
3+
B904 - on lines 10, 11 and 16
4+
"""
5+
6+
try:
7+
raise ValueError
8+
except* ValueError:
9+
if "abc":
10+
raise TypeError
11+
raise UserWarning
12+
except* AssertionError:
13+
raise # Bare `raise` should not be an error
14+
except* Exception as err:
15+
assert err
16+
raise Exception("No cause here...")
17+
except* BaseException as base_err:
18+
# Might use this instead of bare raise with the `.with_traceback()` method
19+
raise base_err
20+
finally:
21+
raise Exception("Nothing to chain from, so no warning here")
22+
23+
try:
24+
raise ValueError
25+
except* ValueError:
26+
# should not emit, since we are not raising something
27+
def proxy():
28+
raise NameError
29+
30+
31+
try:
32+
from preferred_library import Thing
33+
except* ImportError:
34+
try:
35+
from fallback_library import Thing
36+
except* ImportError:
37+
38+
class Thing:
39+
def __getattr__(self, name):
40+
# same as the case above, should not emit.
41+
raise AttributeError
42+
43+
44+
try:
45+
from preferred_library import Thing
46+
except* ImportError:
47+
try:
48+
from fallback_library import Thing
49+
except* ImportError:
50+
51+
def context_switch():
52+
try:
53+
raise ValueError
54+
except* ValueError:
55+
raise Exception

0 commit comments

Comments
 (0)