Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,8 @@ UNRELEASED
~~~~~~~~~~

* B909: detect mutations when a loop unpacks the iterable's elements.
* B040: don't report an exception whose note is shown by ``logger.exception()``,
``exc_info=True``, ``traceback.print_exc()``/``format_exc()`` or ``sys.exc_info()``

26.11.9
~~~~~~~
Expand Down
39 changes: 39 additions & 0 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,43 @@ class B040CaughtException:
has_note: bool


# Calls that use the exception currently being handled without it being passed in,
# e.g. traceback.print_exc() prints it, notes included.
B040_HANDLED_EXCEPTION_CALLS = {
("traceback", "print_exc"),
("traceback", "format_exc"),
("sys", "exc_info"),
("sys", "exception"),
}


def _reports_handled_exception(node: ast.Call) -> bool:
func = node.func
if isinstance(func, ast.Attribute):
# logging.exception(...) / logger.exception(...) / sys.exception()
if func.attr == "exception":
return True
if (
isinstance(func.value, ast.Name)
and (func.value.id, func.attr) in B040_HANDLED_EXCEPTION_CALLS
):
return True
elif isinstance(func, ast.Name) and func.id in (
"print_exc",
"format_exc",
"exc_info",
):
# from traceback import print_exc / from sys import exc_info
return True
# logger.error(..., exc_info=True)
return any(
kw.arg == "exc_info"
and isinstance(kw.value, ast.Constant)
and kw.value.value is True
for kw in node.keywords
)


class B041UnhandledKeyType:
"""
A dictionary key of a type that we do not check for duplicates.
Expand Down Expand Up @@ -1131,6 +1168,8 @@ def visit_Call(self, node: ast.Call) -> None:
if not is_b040_add_note:
self.check_for_b040_usage(node.args)
self.check_for_b040_usage(node.keywords)
if self.b040_caught_exception and _reports_handled_exception(node):
self.b040_caught_exception = None

self.generic_visit(node)

Expand Down
58 changes: 58 additions & 0 deletions tests/eval_files/b040.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,61 @@ def habla(e):
except Exception as e:
e3 = e
e3.add_note("") # should error


# the exception being handled is reported without being passed in
import logging
import sys
import traceback
from traceback import print_exc

logger = logging.getLogger(__name__)

try:
...
except Exception as e:
e.add_note("...") # safe
logger.exception("failed")

try:
...
except Exception as e:
e.add_note("...") # safe
logging.exception("failed")

try:
...
except Exception as e:
e.add_note("...") # safe
logger.error("failed", exc_info=True)

try:
...
except Exception as e:
e.add_note("...") # safe
traceback.print_exc()

try:
...
except Exception as e:
e.add_note("...") # safe
print_exc()

try:
...
except Exception as e:
e.add_note("...") # safe
msg = traceback.format_exc()

try:
...
except Exception as e:
e.add_note("...") # safe
sys.excepthook(*sys.exc_info())

# exc_info=False doesn't log the exception
try:
...
except Exception as e: # B040: 0
e.add_note("...")
logger.error("failed", exc_info=False)
Loading