Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,13 @@ count as active enclosing loops.
so changing it for one key changes it for all of them. Use a dict comprehension instead, e.g.
``{key: [] for key in keys}``.

.. _B047:

**B047**: Unreachable ``except`` handler. An earlier handler already catches a builtin
exception that this one is a subclass of, e.g. ``except ValueError:`` after
``except Exception:``, or a bare ``except:`` after ``except BaseException:``. Only builtin
exceptions are checked, since the hierarchy of other classes isn't known from the source.


Opinionated warnings
~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -539,6 +546,8 @@ UNRELEASED
shared by every key (#387)
* B005: say in the message that only strings with repeated characters are
reported, since ``.strip("abc")`` style character sets are fine (#91)
* B047: New check for ``except`` handlers that can never run because an earlier
handler already catches a builtin parent exception (#349)
* B031: only treat a bare ``groupby()`` call as ``itertools.groupby`` when the
module doesn't bind ``groupby`` to something else, such as its own
``def groupby`` (#356)
Expand Down
42 changes: 42 additions & 0 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -914,6 +914,7 @@ def visit_ClassDef(self, node: ast.ClassDef) -> None:
def visit_Try(self, node: ast.Try | ast.TryStar) -> None:
self.check_for_b012(node)
self.check_for_b025(node)
self.check_for_b047(node)
self.generic_visit(node)

def visit_TryStar(self, node: ast.TryStar) -> None:
Expand Down Expand Up @@ -2212,6 +2213,40 @@ def check_for_b025(self, node: ast.Try | ast.TryStar) -> None:
for duplicate in duplicates:
self.add_error("B025", node, duplicate, self.in_trystar)

def check_for_b047(self, node: ast.Try | ast.TryStar) -> None:
# Only builtin exceptions are checked, since we can't know the class
# hierarchy of anything else from the AST.
caught: list[tuple[str, type]] = []
for handler in node.handlers:
if handler.type is None:
# a bare `except:` catches the same as `except BaseException:`
names = ["BaseException"]
else:
names = [
e.id
for e in _flatten_excepthandler(handler.type)
if isinstance(e, ast.Name)
]

current = []
for name in names:
exc = getattr(builtins, name, None)
if not (
isinstance(exc, type) and _typesafe_issubclass(exc, BaseException)
):
continue
for caught_name, caught_exc in caught:
# the same name caught twice is reported by B025
if (
name != caught_name or handler.type is None
) and _typesafe_issubclass(exc, caught_exc):
self.add_error(
"B047", handler, name, caught_name, self.in_trystar
)
break
current.append((name, exc))
caught.extend(current)

@staticmethod
def _is_infinite_iterator(node: ast.expr) -> bool:
if not (
Expand Down Expand Up @@ -3457,6 +3492,13 @@ def __call__(self, lineno: int, col: int, vars: tuple[object, ...] = ()) -> erro
"them. Use a dict comprehension instead, e.g. `{{key: [] for key in keys}}`."
)
),
"B047": Error(
message=(
"B047 `{0}` is never caught by this handler, since the earlier "
"`except{2} {1}` already catches it. Reorder the handlers or remove "
"the unreachable one."
)
),
# Warnings disabled by default.
"B901": Error(
message=(
Expand Down
93 changes: 93 additions & 0 deletions tests/eval_files/b047.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import os

try:
pass
except BaseException:
raise
except Exception: # B047: 0, "Exception", "BaseException", ""
pass

try:
pass
except BaseException:
raise
except: # B001: 0 # B047: 0, "BaseException", "BaseException", ""
pass

try:
pass
except Exception:
pass
except ValueError: # B047: 0, "ValueError", "Exception", ""
pass
except (KeyError, MyError): # B047: 0, "KeyError", "Exception", ""
pass
except KeyboardInterrupt:
pass

try:
pass
except (TypeError, LookupError) as e:
pass
except IndexError as e: # B047: 0, "IndexError", "LookupError", ""
pass
except (KeyError, UnicodeDecodeError): # B047: 0, "KeyError", "LookupError", ""
pass

# IOError is an alias of OSError
try:
pass
except OSError:
pass
except IOError: # B047: 0, "IOError", "OSError", ""
pass

# starred tuples are expanded
try:
pass
except Exception:
pass
except (*(ValueError,),): # B013: 0, "ValueError", "" # B047: 0, "ValueError", "Exception", ""
pass

# good
try:
pass
except ValueError:
pass
except Exception:
pass
except BaseException:
raise

try:
pass
except KeyError:
pass
except IndexError:
pass
except LookupError:
pass

# not builtins, so the hierarchy is unknown
try:
pass
except MyError:
pass
except MySubError:
pass

try:
pass
except os.error:
pass
except FileNotFoundError:
pass

# the same exception caught twice is B025
try: # B025: 0, "ValueError"
pass
except ValueError:
pass
except ValueError:
pass
13 changes: 13 additions & 0 deletions tests/eval_files/b047_py311.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
try:
pass
except* Exception:
pass
except* ValueError: # B047: 0, "ValueError", "Exception", "*"
pass

try:
pass
except* (TypeError, OSError):
pass
except* (FileNotFoundError, ValueError): # B047: 0, "FileNotFoundError", "OSError", "*"
pass
Loading