Skip to content

Commit 45151fa

Browse files
committed
Apply suggestions from code review
1 parent 346bdcf commit 45151fa

12 files changed

+241
-14
lines changed

CONTRIBUTORS.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,3 +391,5 @@ contributors:
391391
* Shiv Venkatasubrahmanyam
392392

393393
* Jochen Preusche (iilei): contributor
394+
395+
* Ram Rachum (cool-RR)

pylint/checkers/exceptions.py

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ def _is_raising(body: typing.List) -> bool:
7676
return False
7777

7878

79+
def iterate_parents(node: NodeNG) -> typing.Iterator[NodeNG]:
80+
current = node.parent
81+
while current is not None:
82+
yield current
83+
current = current.parent
84+
85+
7986
OVERGENERAL_EXCEPTIONS = ("BaseException", "Exception")
8087
BUILTINS_NAME = builtins.__name__
8188

@@ -152,6 +159,15 @@ def _is_raising(body: typing.List) -> bool:
152159
"immediately. Remove the raise operator or the entire "
153160
"try-except-raise block!",
154161
),
162+
"W0707": (
163+
"Consider explicitly re-raising using the 'from' keyword",
164+
"raise-missing-from",
165+
"Python 3's exception chaining means it shows the traceback of the "
166+
"current exception, but also the original exception. Not using `raise "
167+
"from` makes the traceback inaccurate, because the message implies "
168+
"there is a bug in the exception-handling code itself, which is a "
169+
"separate situation than wrapping an exception.",
170+
),
155171
"W0711": (
156172
'Exception to catch is the result of a binary "%s" operation',
157173
"binary-op-exception",
@@ -279,14 +295,15 @@ def open(self):
279295
"notimplemented-raised",
280296
"bad-exception-context",
281297
"raising-format-tuple",
298+
"raise-missing-from",
282299
)
283300
def visit_raise(self, node):
284301
if node.exc is None:
285302
self._check_misplaced_bare_raise(node)
286303
return
287304

288-
if node.cause:
289-
self._check_bad_exception_context(node)
305+
self._check_bad_exception_cause(node)
306+
self._check_raise_missing_from(node)
290307

291308
expr = node.exc
292309
ExceptionRaiseRefVisitor(self, node).visit(expr)
@@ -320,15 +337,16 @@ def _check_misplaced_bare_raise(self, node):
320337
if not current or not isinstance(current.parent, expected):
321338
self.add_message("misplaced-bare-raise", node=node)
322339

323-
def _check_bad_exception_context(self, node):
324-
"""Verify that the exception context is properly set.
340+
def _check_bad_exception_cause(self, node: astroid.Raise) -> None:
341+
"""Verify that the exception cause is properly set.
325342
326-
An exception context can be only `None` or an exception.
343+
An exception cause can be only `None` or an exception.
327344
"""
345+
if not node.cause:
346+
return
328347
cause = utils.safe_infer(node.cause)
329348
if cause in (astroid.Uninferable, None):
330349
return
331-
332350
if isinstance(cause, astroid.Const):
333351
if cause.value is not None:
334352
self.add_message("bad-exception-context", node=node)
@@ -337,6 +355,46 @@ def _check_bad_exception_context(self, node):
337355
):
338356
self.add_message("bad-exception-context", node=node)
339357

358+
def _check_raise_missing_from(self, node: astroid.Raise) -> None:
359+
if node.cause is not None:
360+
return
361+
if node.exc is None:
362+
# This is a plain `raise`, raising the previously-caught exception. No need for
363+
# a cause.
364+
return
365+
# We'd like to check whether we're inside an `except` clause:
366+
for parent in iterate_parents(node):
367+
if isinstance(parent, astroid.ExceptHandler):
368+
except_parent = parent
369+
# We found a surrounding `except`! We're almost done proving there's a
370+
# `raise-missing-from` here. The only thing we need to protect against is that
371+
# maybe the `raise` is raising the exception that was caught, possibly with
372+
# some shenanigans like `exc.with_traceback(whatever)`. We won't analyze these,
373+
# we'll just assume there's a violation on two simple cases: `raise
374+
# SomeException(whatever)` and `raise SomeException`.
375+
if except_parent.name is None:
376+
# The `except` doesn't have an `as exception:` part, meaning there's no way
377+
# that the `raise` is raising the same exception.
378+
self.add_message("raise-missing-from", node=node)
379+
elif isinstance(node.exc, (astroid.Call)) and isinstance(
380+
node.exc.func, (astroid.Name)
381+
):
382+
# We have a `raise SomeException(whatever)`.
383+
self.add_message("raise-missing-from", node=node)
384+
elif (
385+
isinstance(node.exc, (astroid.Name))
386+
and node.exc.name != except_parent.name.name
387+
):
388+
# We have a `raise SomeException`.
389+
self.add_message("raise-missing-from", node=node)
390+
break
391+
if isinstance(parent, astroid.scoped_nodes.LocalsDictNodeNG):
392+
# If we're inside a function/class definition, we don't want to keep checking
393+
# higher parents for `except` clauses, because if these exist, it means our
394+
# function/class was defined in an `except` clause, rather than the current
395+
# code actually running in an `except` clause.
396+
break
397+
340398
def _check_catching_non_exception(self, handler, exc, part):
341399
if isinstance(exc, astroid.Tuple):
342400
# Check if it is a tuple of exceptions.

tests/functional/m/misplaced_bare_raise.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# pylint: disable=missing-docstring, broad-except, unreachable, try-except-raise
1+
# pylint: disable=missing-docstring, broad-except, unreachable, try-except-raise, raise-missing-from
22
# pylint: disable=unused-variable, too-few-public-methods, invalid-name, useless-object-inheritance
33

44
try:

tests/functional/n/no_else_raise.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
""" Test that superfluous else return are detected. """
22

3-
# pylint:disable=invalid-name,missing-docstring,unused-variable
3+
# pylint:disable=invalid-name,missing-docstring,unused-variable,raise-missing-from
44

55
def foo1(x, y, z):
66
if x: # [no-else-raise]

tests/functional/n/no_else_return.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" Test that superfluous else return are detected. """
22

3-
# pylint:disable=invalid-name,missing-docstring,unused-variable
43

4+
# pylint:disable=invalid-name,missing-docstring,unused-variable
55
def foo1(x, y, z):
66
if x: # [no-else-return]
77
a = 1
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# pylint:disable=missing-docstring, unreachable, using-constant-test, invalid-name, bare-except
2+
# pylint:disable=try-except-raise, undefined-variable, too-few-public-methods, superfluous-parens
3+
4+
################################################################################
5+
# Positives:
6+
7+
try:
8+
1 / 0
9+
except ZeroDivisionError:
10+
# +1: [raise-missing-from]
11+
raise KeyError
12+
13+
try:
14+
1 / 0
15+
except ZeroDivisionError:
16+
# Our algorithm doesn't have to be careful about the complicated expression below, because
17+
# the exception above wasn't bound to a name.
18+
# +1: [raise-missing-from]
19+
raise (foo + bar).baz
20+
21+
try:
22+
1 / 0
23+
except ZeroDivisionError as e:
24+
# +1: [raise-missing-from]
25+
raise KeyError
26+
27+
try:
28+
1 / 0
29+
except ZeroDivisionError as e:
30+
# +1: [raise-missing-from]
31+
raise KeyError
32+
else:
33+
pass
34+
finally:
35+
pass
36+
37+
try:
38+
1 / 0
39+
except ZeroDivisionError as e:
40+
if 1:
41+
if 1:
42+
with whatever:
43+
try:
44+
# +1: [raise-missing-from]
45+
raise KeyError
46+
except:
47+
pass
48+
49+
try:
50+
1 / 0
51+
except ZeroDivisionError as e:
52+
# +1: [raise-missing-from]
53+
raise KeyError()
54+
55+
try:
56+
1 / 0
57+
except ZeroDivisionError as e:
58+
# +1: [raise-missing-from]
59+
raise KeyError(whatever, whatever=whatever)
60+
61+
62+
################################################################################
63+
# Negatives (Same cases as above, except with `from`):
64+
65+
try:
66+
1 / 0
67+
except ZeroDivisionError:
68+
raise KeyError from foo
69+
70+
try:
71+
1 / 0
72+
except ZeroDivisionError:
73+
raise (foo + bar).baz from foo
74+
75+
try:
76+
1 / 0
77+
except ZeroDivisionError as e:
78+
raise KeyError from foo
79+
80+
try:
81+
1 / 0
82+
except ZeroDivisionError as e:
83+
raise KeyError from foo
84+
else:
85+
pass
86+
finally:
87+
pass
88+
89+
try:
90+
1 / 0
91+
except ZeroDivisionError as e:
92+
if 1:
93+
if 1:
94+
with whatever:
95+
try:
96+
raise KeyError from foo
97+
except:
98+
pass
99+
100+
try:
101+
1 / 0
102+
except ZeroDivisionError as e:
103+
raise KeyError() from foo
104+
105+
try:
106+
1 / 0
107+
except ZeroDivisionError as e:
108+
raise KeyError(whatever, whatever=whatever) from foo
109+
110+
111+
################################################################################
112+
# Other negatives:
113+
114+
try:
115+
1 / 0
116+
except ZeroDivisionError:
117+
raise
118+
119+
try:
120+
1 / 0
121+
except ZeroDivisionError as e:
122+
raise
123+
124+
try:
125+
1 / 0
126+
except ZeroDivisionError as e:
127+
raise e
128+
129+
try:
130+
1 / 0
131+
except ZeroDivisionError as e:
132+
if 1:
133+
if 1:
134+
if 1:
135+
raise e
136+
137+
try:
138+
1 / 0
139+
except ZeroDivisionError as e:
140+
raise e.with_traceback(e.__traceback__)
141+
142+
try:
143+
1 / 0
144+
except ZeroDivisionError as e:
145+
raise (e + 7)
146+
147+
try:
148+
1 / 0
149+
except ZeroDivisionError as e:
150+
def f():
151+
raise KeyError
152+
153+
try:
154+
1 / 0
155+
except ZeroDivisionError as e:
156+
class Foo:
157+
raise KeyError
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
raise-missing-from:11::Exception needs to be raised with cause
2+
raise-missing-from:19::Exception needs to be raised with cause
3+
raise-missing-from:25::Exception needs to be raised with cause
4+
raise-missing-from:31::Exception needs to be raised with cause
5+
raise-missing-from:45::Exception needs to be raised with cause
6+
raise-missing-from:53::Exception needs to be raised with cause
7+
raise-missing-from:59::Exception needs to be raised with cause

tests/functional/r/regression_3416_unused_argument_raise.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
https://github.com/PyCQA/pylint/issues/3416
44
"""
55

6+
# pylint:disable=raise-missing-from
7+
68
# +1: [unused-argument, unused-argument, unused-argument]
79
def fun(arg_a, arg_b, arg_c) -> None:
810
"""Routine docstring"""
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
unused-argument:7:fun:Unused argument 'arg_a'
2-
unused-argument:7:fun:Unused argument 'arg_b'
3-
unused-argument:7:fun:Unused argument 'arg_c'
1+
unused-argument:9:fun:Unused argument 'arg_a'
2+
unused-argument:9:fun:Unused argument 'arg_b'
3+
unused-argument:9:fun:Unused argument 'arg_c'

tests/functional/s/stop_iteration_inside_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""
22
Test that no StopIteration is raised inside a generator
33
"""
4-
# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position,not-callable
4+
# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position,not-callable,raise-missing-from
55
import asyncio
66

77
class RebornStopIteration(StopIteration):

0 commit comments

Comments
 (0)