Skip to content

Commit e35343c

Browse files
Zac-HDcooperlees
authored andcommitted
Add new checks for except (...): clauses (#105)
1 parent 04dd13b commit e35343c

File tree

6 files changed

+185
-4
lines changed

6 files changed

+185
-4
lines changed

README.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ To silence an exception, do it explicitly in the `except` block. To properly use
106106
a `break`, `continue` or `return` refactor your code so these statements are not
107107
in the `finally` block.
108108

109+
**B013**: A length-one tuple literal is redundant. Write `except SomeError:`
110+
instead of `except (SomeError,):`.
111+
112+
**B014**: Redundant exception types in `except (Exception, TypeError):`.
113+
Write `except Exception:`, which catches exactly the same exceptions.
114+
115+
109116
Python 3 compatibility warnings
110117
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
111118

@@ -228,6 +235,12 @@ MIT
228235
Change Log
229236
----------
230237

238+
Future
239+
~~~~~~
240+
241+
* For B001, also check for ``except ():``
242+
* Introduce B013 and B014 to check tuples in ``except (..., ):`` statements
243+
231244
20.1.0
232245
~~~~~~
233246

bugbear.py

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ast
2+
import builtins
23
from collections import namedtuple
34
from contextlib import suppress
45
from functools import lru_cache, partial
@@ -136,7 +137,50 @@ def visit(self, node):
136137

137138
def visit_ExceptHandler(self, node):
138139
if node.type is None:
139-
self.errors.append(B001(node.lineno, node.col_offset))
140+
self.errors.append(
141+
B001(node.lineno, node.col_offset, vars=("bare `except:`",))
142+
)
143+
elif isinstance(node.type, ast.Tuple):
144+
names = []
145+
for e in node.type.elts:
146+
if isinstance(e, ast.Name):
147+
names.append(e.id)
148+
else:
149+
assert isinstance(e, ast.Attribute)
150+
names.append("{}.{}".format(e.value.id, e.attr))
151+
as_ = " as " + node.name if node.name is not None else ""
152+
if len(names) == 0:
153+
vs = ("`except (){}:`".format(as_),)
154+
self.errors.append(B001(node.lineno, node.col_offset, vars=vs))
155+
elif len(names) == 1:
156+
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
157+
else:
158+
# See if any of the given exception names could be removed, e.g. from:
159+
# (MyError, MyError) # duplicate names
160+
# (MyError, BaseException) # everything derives from the Base
161+
# (Exception, TypeError) # builtins where one subclasses another
162+
# but note that other cases are impractical to hande from the AST.
163+
# We expect this is mostly useful for users who do not have the
164+
# builtin exception hierarchy memorised, and include a 'shadowed'
165+
# subtype without realising that it's redundant.
166+
good = sorted(set(names), key=names.index)
167+
if "BaseException" in good:
168+
good = ["BaseException"]
169+
for name, other in itertools.permutations(tuple(good), 2):
170+
if issubclass(
171+
getattr(builtins, name, type), getattr(builtins, other, ())
172+
):
173+
if name in good:
174+
good.remove(name)
175+
if good != names:
176+
desc = good[0] if len(good) == 1 else "({})".format(", ".join(good))
177+
self.errors.append(
178+
B014(
179+
node.lineno,
180+
node.col_offset,
181+
vars=(", ".join(names), as_, desc),
182+
)
183+
)
140184
self.generic_visit(node)
141185

142186
def visit_UAdd(self, node):
@@ -459,7 +503,7 @@ def visit(self, node):
459503

460504

461505
B001 = Error(
462-
message="B001 Do not use bare `except:`, it also catches unexpected "
506+
message="B001 Do not use {}, it also catches unexpected "
463507
"events like memory errors, interrupts, system exit, and so on. "
464508
"Prefer `except Exception:`. If you're sure what you're doing, "
465509
"be explicit and write `except BaseException:`."
@@ -542,6 +586,14 @@ def visit(self, node):
542586
"to be silenced. Exceptions should be silenced in except blocks. Control "
543587
"statements can be moved outside the finally block."
544588
)
589+
B013 = Error(
590+
message="B013 A length-one tuple literal is redundant. "
591+
"Write `except {0}:` instead of `except ({0},):`."
592+
)
593+
B014 = Error(
594+
message="B014 Redundant exception types in `except ({0}){1}:`. "
595+
"Write `except {2}{1}:`, which catches exactly the same exceptions."
596+
)
545597

546598
# Those could be false positives but it's more dangerous to let them slip
547599
# through if they're not.

tests/b001.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
Should emit:
3-
B001 - on lines 8 and 40
3+
B001 - on lines 8, 40, and 54
44
"""
55

66
try:
@@ -47,3 +47,10 @@ def func(**kwargs):
4747
except: # noqa
4848
# warning silenced
4949
return
50+
51+
52+
try:
53+
pass
54+
except ():
55+
# Literal empty tuple is just like bare except:
56+
pass

tests/b013.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
try:
15+
pass
16+
except (ValueError):
17+
# not using a tuple means it's OK (if odd)
18+
pass
19+
20+
try:
21+
pass
22+
except ValueError:
23+
# no warning here, all good
24+
pass
25+
26+
try:
27+
pass
28+
except (re.error,):
29+
# pointless use of tuple with dotted attribute
30+
pass

tests/b014.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Should emit:
3+
B014 - on lines 10, 16, 27, 41, and 48
4+
"""
5+
6+
import re
7+
8+
try:
9+
pass
10+
except (Exception, TypeError):
11+
# TypeError is a subclass of Exception, so it doesn't add anything
12+
pass
13+
14+
try:
15+
pass
16+
except (OSError, OSError) as err:
17+
# Duplicate exception types are useless
18+
pass
19+
20+
21+
class MyError(Exception):
22+
pass
23+
24+
25+
try:
26+
pass
27+
except (MyError, MyError):
28+
# Detect duplicate non-builtin errors
29+
pass
30+
31+
32+
try:
33+
pass
34+
except (MyError, Exception) as e:
35+
# Don't assume that we're all subclasses of Exception
36+
pass
37+
38+
39+
try:
40+
pass
41+
except (MyError, BaseException) as e:
42+
# But we *can* assume that everything is a subclass of BaseException
43+
pass
44+
45+
46+
try:
47+
pass
48+
except (re.error, re.error):
49+
# Duplicate exception types as attributes
50+
pass

tests/test_bugbear.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
B010,
1717
B011,
1818
B012,
19+
B013,
20+
B014,
1921
B301,
2022
B302,
2123
B303,
@@ -39,7 +41,12 @@ def test_b001(self):
3941
filename = Path(__file__).absolute().parent / "b001.py"
4042
bbc = BugBearChecker(filename=str(filename))
4143
errors = list(bbc.run())
42-
self.assertEqual(errors, self.errors(B001(8, 0), B001(40, 4)))
44+
expected = self.errors(
45+
B001(8, 0, vars=("bare `except:`",)),
46+
B001(40, 4, vars=("bare `except:`",)),
47+
B001(54, 0, vars=("`except ():`",)),
48+
)
49+
self.assertEqual(errors, expected)
4350

4451
def test_b002(self):
4552
filename = Path(__file__).absolute().parent / "b002.py"
@@ -138,6 +145,28 @@ def test_b012(self):
138145
]
139146
self.assertEqual(errors, self.errors(*all_errors))
140147

148+
def test_b013(self):
149+
filename = Path(__file__).absolute().parent / "b013.py"
150+
bbc = BugBearChecker(filename=str(filename))
151+
errors = list(bbc.run())
152+
expected = self.errors(
153+
B013(10, 0, vars=("ValueError",)), B013(28, 0, vars=("re.error",))
154+
)
155+
self.assertEqual(errors, expected)
156+
157+
def test_b014(self):
158+
filename = Path(__file__).absolute().parent / "b014.py"
159+
bbc = BugBearChecker(filename=str(filename))
160+
errors = list(bbc.run())
161+
expected = self.errors(
162+
B014(10, 0, vars=("Exception, TypeError", "", "Exception")),
163+
B014(16, 0, vars=("OSError, OSError", " as err", "OSError")),
164+
B014(27, 0, vars=("MyError, MyError", "", "MyError")),
165+
B014(41, 0, vars=("MyError, BaseException", " as e", "BaseException")),
166+
B014(48, 0, vars=("re.error, re.error", "", "re.error")),
167+
)
168+
self.assertEqual(errors, expected)
169+
141170
def test_b301_b302_b305(self):
142171
filename = Path(__file__).absolute().parent / "b301_b302_b305.py"
143172
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)