Skip to content

Commit b462a5e

Browse files
authored
Add B904 - Use raise ... from err in except clauses (#181)
* Strip trailing whitespace * Add B904 - use exc chaining * Format with black * Allow explicit re-raise This is useful when you want to explicitly modify the traceback, as done by Hypothesis and Trio * Disable raises check by default
1 parent ab0868d commit b462a5e

File tree

5 files changed

+74
-7
lines changed

5 files changed

+74
-7
lines changed

Diff for: .flake8

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Keep in sync with setup.cfg which is used for source packages.
33

44
[flake8]
5-
ignore = E203, E302, E501, E999
5+
ignore = E203, E302, E501, E999, W503
66
max-line-length = 88
77
max-complexity = 12
88
select = B,C,E,F,W,B9

Diff for: README.rst

+15-5
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ waste CPU instructions. Either prepend ``assert`` or remove it.
123123
**B016**: Cannot raise a literal. Did you intend to return it or raise
124124
an Exception?
125125

126-
**B017**: ``self.assertRaises(Exception):`` should be considered evil. It can lead
127-
to your test passing even if the code being tested is never executed due to a typo.
128-
Either assert for a more specific exception (builtin or custom), use
126+
**B017**: ``self.assertRaises(Exception):`` should be considered evil. It can lead
127+
to your test passing even if the code being tested is never executed due to a typo.
128+
Either assert for a more specific exception (builtin or custom), use
129129
``assertRaisesRegex``, or use the context manager form of assertRaises
130130
(``with self.assertRaises(Exception) as ex:``) with an assertion against the
131131
data available in ``ex``.
@@ -157,6 +157,11 @@ nothing else. If the attributes should be mutable, define the attributes
157157
in ``__slots__`` to save per-instance memory and to prevent accidentally
158158
creating additional attributes on instances.
159159

160+
**B904**: Within an ``except`` clause, raise exceptions with ``raise ... from err``
161+
or ``raise ... from None`` to distinguish them from errors in exception handling.
162+
See [the exception chaining tutorial](https://docs.python.org/3/tutorial/errors.html#exception-chaining)
163+
for details.
164+
160165
**B950**: Line too long. This is a pragmatic equivalent of
161166
``pycodestyle``'s E501: it considers "max-line-length" but only triggers
162167
when the value has been exceeded by **more than 10%**. You will no
@@ -217,10 +222,15 @@ MIT
217222
Change Log
218223
----------
219224

225+
Future
226+
~~~~~~
227+
228+
* Add B904: check for ``raise`` without ``from`` in an ``except`` clause
229+
220230
21.4.3
221231
~~~~~~
222232

223-
* Verify the element in item_context.args is of type ast.Name for b017
233+
* Verify the element in item_context.args is of type ast.Name for b017
224234

225235
21.4.2
226236
~~~~~~
@@ -231,7 +241,7 @@ Change Log
231241
~~~~~~
232242

233243
* Add B017: check for gotta-catch-em-all assertRaises(Exception)
234-
244+
235245
21.3.2
236246
~~~~~~
237247

Diff for: bugbear.py

+25-1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ def visit_Compare(self, node):
292292

293293
def visit_Raise(self, node):
294294
self.check_for_b016(node)
295+
self.check_for_b904(node)
295296
self.generic_visit(node)
296297

297298
def visit_With(self, node):
@@ -426,6 +427,21 @@ def check_for_b017(self, node):
426427
):
427428
self.errors.append(B017(node.lineno, node.col_offset))
428429

430+
def check_for_b904(self, node):
431+
"""Checks `raise` without `from` inside an `except` clause.
432+
433+
In these cases, you should use explicit exception chaining from the
434+
earlier error, or suppress it with `raise ... from None`. See
435+
https://docs.python.org/3/tutorial/errors.html#exception-chaining
436+
"""
437+
if (
438+
node.cause is None
439+
and node.exc is not None
440+
and not (isinstance(node.exc, ast.Name) and node.exc.id.islower())
441+
and any(isinstance(n, ast.ExceptHandler) for n in self.node_stack)
442+
):
443+
self.errors.append(B904(node.lineno, node.col_offset))
444+
429445
def walk_function_body(self, node):
430446
def _loop(parent, node):
431447
if isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
@@ -775,6 +791,14 @@ def visit(self, node):
775791
)
776792
)
777793

794+
B904 = Error(
795+
message=(
796+
"B904 Within an `except` clause, raise exceptions with `raise ... from err` "
797+
"or `raise ... from None` to distinguish them from errors in exception handling. "
798+
"See https://docs.python.org/3/tutorial/errors.html#exception-chaining for details."
799+
)
800+
)
801+
778802
B950 = Error(message="B950 line too long ({} > {} characters)")
779803

780-
disabled_by_default = ["B901", "B902", "B903", "B950"]
804+
disabled_by_default = ["B901", "B902", "B903", "B904", "B950"]

Diff for: tests/b904.py

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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")

Diff for: tests/test_bugbear.py

+12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
B015,
2929
B016,
3030
B017,
31+
B904,
3132
B901,
3233
B902,
3334
B903,
@@ -260,6 +261,17 @@ def test_b903(self):
260261
errors = list(bbc.run())
261262
self.assertEqual(errors, self.errors(B903(32, 0), B903(38, 0)))
262263

264+
def test_b904(self):
265+
filename = Path(__file__).absolute().parent / "b904.py"
266+
bbc = BugBearChecker(filename=str(filename))
267+
errors = list(bbc.run())
268+
expected = [
269+
B904(10, 8),
270+
B904(11, 4),
271+
B904(16, 4),
272+
]
273+
self.assertEqual(errors, self.errors(*expected))
274+
263275
def test_b950(self):
264276
filename = Path(__file__).absolute().parent / "b950.py"
265277
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)