Skip to content

Commit 9ea5a34

Browse files
Introduce B030; fix crash on weird except handlers (#346)
* Introduce B029; fix crash on weird except handlers * black * fix merge * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 315f4e7 commit 9ea5a34

File tree

4 files changed

+94
-41
lines changed

4 files changed

+94
-41
lines changed

README.rst

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ It is therefore recommended to use a stacklevel of 2 or greater to provide more
179179

180180
**B029**: Using ``except: ()`` with an empty tuple does not handle/catch anything. Add exceptions to handle.
181181

182+
**B030**: Except handlers should only be exception classes or tuples of exception classes.
183+
182184
Opinionated warnings
183185
~~~~~~~~~~~~~~~~~~~~
184186

bugbear.py

+67-41
Original file line numberDiff line numberDiff line change
@@ -189,13 +189,59 @@ def _is_identifier(arg):
189189
return re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", arg.s) is not None
190190

191191

192+
def _flatten_excepthandler(node):
193+
if isinstance(node, ast.Tuple):
194+
for elt in node.elts:
195+
yield from _flatten_excepthandler(elt)
196+
else:
197+
yield node
198+
199+
200+
def _check_redundant_excepthandlers(names, node):
201+
# See if any of the given exception names could be removed, e.g. from:
202+
# (MyError, MyError) # duplicate names
203+
# (MyError, BaseException) # everything derives from the Base
204+
# (Exception, TypeError) # builtins where one subclasses another
205+
# (IOError, OSError) # IOError is an alias of OSError since Python3.3
206+
# but note that other cases are impractical to handle from the AST.
207+
# We expect this is mostly useful for users who do not have the
208+
# builtin exception hierarchy memorised, and include a 'shadowed'
209+
# subtype without realising that it's redundant.
210+
good = sorted(set(names), key=names.index)
211+
if "BaseException" in good:
212+
good = ["BaseException"]
213+
# Remove redundant exceptions that the automatic system either handles
214+
# poorly (usually aliases) or can't be checked (e.g. it's not an
215+
# built-in exception).
216+
for primary, equivalents in B014.redundant_exceptions.items():
217+
if primary in good:
218+
good = [g for g in good if g not in equivalents]
219+
220+
for name, other in itertools.permutations(tuple(good), 2):
221+
if _typesafe_issubclass(
222+
getattr(builtins, name, type), getattr(builtins, other, ())
223+
):
224+
if name in good:
225+
good.remove(name)
226+
if good != names:
227+
desc = good[0] if len(good) == 1 else "({})".format(", ".join(good))
228+
as_ = " as " + node.name if node.name is not None else ""
229+
return B014(
230+
node.lineno,
231+
node.col_offset,
232+
vars=(", ".join(names), as_, desc),
233+
)
234+
return None
235+
236+
192237
def _to_name_str(node):
193238
# Turn Name and Attribute nodes to strings, e.g "ValueError" or
194239
# "pkg.mod.error", handling any depth of attribute accesses.
195240
if isinstance(node, ast.Name):
196241
return node.id
197242
if isinstance(node, ast.Call):
198243
return _to_name_str(node.func)
244+
assert isinstance(node, ast.Attribute), f"Unexpected node type: {type(node)}"
199245
try:
200246
return _to_name_str(node.value) + "." + node.attr
201247
except AttributeError:
@@ -277,48 +323,27 @@ def visit(self, node):
277323
def visit_ExceptHandler(self, node):
278324
if node.type is None:
279325
self.errors.append(B001(node.lineno, node.col_offset))
280-
elif isinstance(node.type, ast.Tuple):
281-
names = [_to_name_str(e) for e in node.type.elts]
282-
as_ = " as " + node.name if node.name is not None else ""
283-
if len(names) == 0:
284-
self.errors.append(B029(node.lineno, node.col_offset))
285-
elif len(names) == 1:
286-
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
326+
self.generic_visit(node)
327+
return
328+
handlers = _flatten_excepthandler(node.type)
329+
good_handlers = []
330+
bad_handlers = []
331+
for handler in handlers:
332+
if isinstance(handler, (ast.Name, ast.Attribute)):
333+
good_handlers.append(handler)
287334
else:
288-
# See if any of the given exception names could be removed, e.g. from:
289-
# (MyError, MyError) # duplicate names
290-
# (MyError, BaseException) # everything derives from the Base
291-
# (Exception, TypeError) # builtins where one subclasses another
292-
# (IOError, OSError) # IOError is an alias of OSError since Python3.3
293-
# but note that other cases are impractical to handle from the AST.
294-
# We expect this is mostly useful for users who do not have the
295-
# builtin exception hierarchy memorised, and include a 'shadowed'
296-
# subtype without realising that it's redundant.
297-
good = sorted(set(names), key=names.index)
298-
if "BaseException" in good:
299-
good = ["BaseException"]
300-
# Remove redundant exceptions that the automatic system either handles
301-
# poorly (usually aliases) or can't be checked (e.g. it's not an
302-
# built-in exception).
303-
for primary, equivalents in B014.redundant_exceptions.items():
304-
if primary in good:
305-
good = [g for g in good if g not in equivalents]
306-
307-
for name, other in itertools.permutations(tuple(good), 2):
308-
if _typesafe_issubclass(
309-
getattr(builtins, name, type), getattr(builtins, other, ())
310-
):
311-
if name in good:
312-
good.remove(name)
313-
if good != names:
314-
desc = good[0] if len(good) == 1 else "({})".format(", ".join(good))
315-
self.errors.append(
316-
B014(
317-
node.lineno,
318-
node.col_offset,
319-
vars=(", ".join(names), as_, desc),
320-
)
321-
)
335+
bad_handlers.append(handler)
336+
if bad_handlers:
337+
self.errors.append(B030(node.lineno, node.col_offset))
338+
names = [_to_name_str(e) for e in good_handlers]
339+
if len(names) == 0 and not bad_handlers:
340+
self.errors.append(B029(node.lineno, node.col_offset))
341+
elif len(names) == 1 and not bad_handlers and isinstance(node.type, ast.Tuple):
342+
self.errors.append(B013(node.lineno, node.col_offset, vars=names))
343+
else:
344+
maybe_error = _check_redundant_excepthandlers(names, node)
345+
if maybe_error is not None:
346+
self.errors.append(maybe_error)
322347
self.generic_visit(node)
323348

324349
def visit_UAdd(self, node):
@@ -1533,6 +1558,7 @@ def visit_Lambda(self, node):
15331558
"anything. Add exceptions to handle."
15341559
)
15351560
)
1561+
B030 = Error(message="B030 Except handlers should only be names of exception classes")
15361562

15371563
# Warnings disabled by default.
15381564
B901 = Error(

tests/b030.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
try:
2+
pass
3+
except (ValueError, (RuntimeError, (KeyError, TypeError))): # ok
4+
pass
5+
6+
try:
7+
pass
8+
except 1: # error
9+
pass
10+
11+
try:
12+
pass
13+
except (1, ValueError): # error
14+
pass

tests/test_bugbear.py

+11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
B027,
4242
B028,
4343
B029,
44+
B030,
4445
B901,
4546
B902,
4647
B903,
@@ -448,6 +449,16 @@ def test_b029(self):
448449
)
449450
self.assertEqual(errors, expected)
450451

452+
def test_b030(self):
453+
filename = Path(__file__).absolute().parent / "b030.py"
454+
bbc = BugBearChecker(filename=str(filename))
455+
errors = list(bbc.run())
456+
expected = self.errors(
457+
B030(8, 0),
458+
B030(13, 0),
459+
)
460+
self.assertEqual(errors, expected)
461+
451462
@unittest.skipIf(sys.version_info < (3, 8), "not implemented for <3.8")
452463
def test_b907(self):
453464
filename = Path(__file__).absolute().parent / "b907.py"

0 commit comments

Comments
 (0)