Skip to content

Commit fa8861e

Browse files
committed
Warn against reusing exception names after the except: block on Python 3
1 parent cddd729 commit fa8861e

File tree

2 files changed

+178
-5
lines changed

2 files changed

+178
-5
lines changed

pyflakes/checker.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ def getNodeName(node):
284284
# Returns node.id, or node.name, or None
285285
if hasattr(node, 'id'): # One of the many nodes with an id
286286
return node.id
287-
if hasattr(node, 'name'): # a ExceptHandler node
287+
if hasattr(node, 'name'): # an ExceptHandler node
288288
return node.name
289289

290290

@@ -1095,8 +1095,29 @@ def TRY(self, node):
10951095
TRYEXCEPT = TRY
10961096

10971097
def EXCEPTHANDLER(self, node):
1098-
# 3.x: in addition to handling children, we must handle the name of
1099-
# the exception, which is not a Name node, but a simple string.
1100-
if isinstance(node.name, str):
1101-
self.handleNodeStore(node)
1098+
if PY2 or node.name is None:
1099+
self.handleChildren(node)
1100+
return
1101+
1102+
# 3.x: the name of the exception, which is not a Name node, but
1103+
# a simple string, creates a local that is only bound within the scope
1104+
# of the except: block.
1105+
1106+
for scope in self.scopeStack[::-1]:
1107+
if node.name in scope:
1108+
is_name_previously_defined = True
1109+
break
1110+
else:
1111+
is_name_previously_defined = False
1112+
1113+
self.handleNodeStore(node)
11021114
self.handleChildren(node)
1115+
if not is_name_previously_defined:
1116+
# See discussion on https://github.com/pyflakes/pyflakes/pull/59.
1117+
1118+
# We're removing the local name since it's being unbound
1119+
# after leaving the except: block and it's always unbound
1120+
# if the except: block is never entered. This will cause an
1121+
# "undefined name" error raised if the checked code tries to
1122+
# use the name afterwards.
1123+
del self.scope[node.name]

pyflakes/test/test_undefined_names.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,158 @@ def test_undefinedInListComp(self):
2222
''',
2323
m.UndefinedName)
2424

25+
@skipIf(version_info < (3,),
26+
'in Python 2 exception names stay bound after the except: block')
27+
def test_undefinedExceptionName(self):
28+
"""Exception names can't be used after the except: block."""
29+
self.flakes('''
30+
try:
31+
raise ValueError('ve')
32+
except ValueError as exc:
33+
pass
34+
exc
35+
''',
36+
m.UndefinedName)
37+
38+
def test_namesDeclaredInExceptBlocks(self):
39+
"""Locals declared in except: blocks can be used after the block.
40+
41+
This shows the example in test_undefinedExceptionName is
42+
different."""
43+
self.flakes('''
44+
try:
45+
raise ValueError('ve')
46+
except ValueError as exc:
47+
e = exc
48+
e
49+
''')
50+
51+
@skip('error reporting disabled due to false positives below')
52+
def test_undefinedExceptionNameObscuringLocalVariable(self):
53+
"""Exception names obscure locals, can't be used after.
54+
55+
Last line will raise UnboundLocalError on Python 3 after exiting
56+
the except: block. Note next two examples for false positives to
57+
watch out for."""
58+
self.flakes('''
59+
exc = 'Original value'
60+
try:
61+
raise ValueError('ve')
62+
except ValueError as exc:
63+
pass
64+
exc
65+
''',
66+
m.UndefinedName)
67+
68+
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive1(self):
69+
"""Exception names obscure locals, can't be used after. Unless.
70+
71+
Last line will never raise UnboundLocalError because it's only
72+
entered if no exception was raised."""
73+
self.flakes('''
74+
exc = 'Original value'
75+
try:
76+
raise ValueError('ve')
77+
except ValueError as exc:
78+
print('exception logged')
79+
raise
80+
exc
81+
''')
82+
83+
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive2(self):
84+
"""Exception names obscure locals, can't be used after. Unless.
85+
86+
Last line will never raise UnboundLocalError because `error` is
87+
only falsy if the `except:` block has not been entered."""
88+
self.flakes('''
89+
exc = 'Original value'
90+
error = None
91+
try:
92+
raise ValueError('ve')
93+
except ValueError as exc:
94+
error = 'exception logged'
95+
if error:
96+
print(error)
97+
else:
98+
exc
99+
''')
100+
101+
@skip('error reporting disabled due to false positives below')
102+
def test_undefinedExceptionNameObscuringGlobalVariable(self):
103+
"""Exception names obscure globals, can't be used after.
104+
105+
Last line will raise UnboundLocalError on both Python 2 and
106+
Python 3 because the existence of that exception name creates
107+
a local scope placeholder for it, obscuring any globals, etc."""
108+
self.flakes('''
109+
exc = 'Original value'
110+
def func():
111+
try:
112+
pass # nothing is raised
113+
except ValueError as exc:
114+
pass # block never entered, exc stays unbound
115+
exc
116+
''',
117+
m.UndefinedLocal)
118+
119+
@skip('error reporting disabled due to false positives below')
120+
def test_undefinedExceptionNameObscuringGlobalVariable2(self):
121+
"""Exception names obscure globals, can't be used after.
122+
123+
Last line will raise NameError on Python 3 because the name is
124+
locally unbound after the `except:` block, even if it's
125+
nonlocal. We should issue an error in this case because code
126+
only working correctly if an exception isn't raised, is invalid.
127+
Unless it's explicitly silenced, see false positives below."""
128+
self.flakes('''
129+
exc = 'Original value'
130+
def func():
131+
global exc
132+
try:
133+
raise ValueError('ve')
134+
except ValueError as exc:
135+
pass # block never entered, exc stays unbound
136+
exc
137+
''',
138+
m.UndefinedLocal)
139+
140+
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive1(self):
141+
"""Exception names obscure globals, can't be used after. Unless.
142+
143+
Last line will never raise NameError because it's only entered
144+
if no exception was raised."""
145+
self.flakes('''
146+
exc = 'Original value'
147+
def func():
148+
global exc
149+
try:
150+
raise ValueError('ve')
151+
except ValueError as exc:
152+
print('exception logged')
153+
raise
154+
exc
155+
''')
156+
157+
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive2(self):
158+
"""Exception names obscure globals, can't be used after. Unless.
159+
160+
Last line will never raise NameError because `error` is only
161+
falsy if the `except:` block has not been entered."""
162+
self.flakes('''
163+
exc = 'Original value'
164+
def func():
165+
global exc
166+
error = None
167+
try:
168+
raise ValueError('ve')
169+
except ValueError as exc:
170+
error = 'exception logged'
171+
if error:
172+
print(error)
173+
else:
174+
exc
175+
''')
176+
25177
def test_functionsNeedGlobalScope(self):
26178
self.flakes('''
27179
class a:

0 commit comments

Comments
 (0)