Skip to content

Commit 3d1f8de

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

File tree

2 files changed

+195
-5
lines changed

2 files changed

+195
-5
lines changed

pyflakes/checker.py

+26-5
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

+169
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,175 @@ 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+
@skipIf(version_info < (3,),
69+
'in Python 2 exception names stay bound after the except: block')
70+
def test_undefinedExceptionNameObscuringLocalVariable2(self):
71+
"""Exception names are unbound after the `except:` block.
72+
73+
Last line will raise UnboundLocalError on Python 3 but would print out
74+
've' on Python 2."""
75+
self.flakes('''
76+
try:
77+
raise ValueError('ve')
78+
except ValueError as exc:
79+
pass
80+
print(exc)
81+
exc = 'Original value'
82+
''',
83+
m.UndefinedName)
84+
85+
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive1(self):
86+
"""Exception names obscure locals, can't be used after. Unless.
87+
88+
Last line will never raise UnboundLocalError because it's only
89+
entered if no exception was raised."""
90+
self.flakes('''
91+
exc = 'Original value'
92+
try:
93+
raise ValueError('ve')
94+
except ValueError as exc:
95+
print('exception logged')
96+
raise
97+
exc
98+
''')
99+
100+
def test_undefinedExceptionNameObscuringLocalVariableFalsePositive2(self):
101+
"""Exception names obscure locals, can't be used after. Unless.
102+
103+
Last line will never raise UnboundLocalError because `error` is
104+
only falsy if the `except:` block has not been entered."""
105+
self.flakes('''
106+
exc = 'Original value'
107+
error = None
108+
try:
109+
raise ValueError('ve')
110+
except ValueError as exc:
111+
error = 'exception logged'
112+
if error:
113+
print(error)
114+
else:
115+
exc
116+
''')
117+
118+
@skip('error reporting disabled due to false positives below')
119+
def test_undefinedExceptionNameObscuringGlobalVariable(self):
120+
"""Exception names obscure globals, can't be used after.
121+
122+
Last line will raise UnboundLocalError on both Python 2 and
123+
Python 3 because the existence of that exception name creates
124+
a local scope placeholder for it, obscuring any globals, etc."""
125+
self.flakes('''
126+
exc = 'Original value'
127+
def func():
128+
try:
129+
pass # nothing is raised
130+
except ValueError as exc:
131+
pass # block never entered, exc stays unbound
132+
exc
133+
''',
134+
m.UndefinedLocal)
135+
136+
@skip('error reporting disabled due to false positives below')
137+
def test_undefinedExceptionNameObscuringGlobalVariable2(self):
138+
"""Exception names obscure globals, can't be used after.
139+
140+
Last line will raise NameError on Python 3 because the name is
141+
locally unbound after the `except:` block, even if it's
142+
nonlocal. We should issue an error in this case because code
143+
only working correctly if an exception isn't raised, is invalid.
144+
Unless it's explicitly silenced, see false positives below."""
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+
pass # block never entered, exc stays unbound
153+
exc
154+
''',
155+
m.UndefinedLocal)
156+
157+
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive1(self):
158+
"""Exception names obscure globals, can't be used after. Unless.
159+
160+
Last line will never raise NameError because it's only entered
161+
if no exception was raised."""
162+
self.flakes('''
163+
exc = 'Original value'
164+
def func():
165+
global exc
166+
try:
167+
raise ValueError('ve')
168+
except ValueError as exc:
169+
print('exception logged')
170+
raise
171+
exc
172+
''')
173+
174+
def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive2(self):
175+
"""Exception names obscure globals, can't be used after. Unless.
176+
177+
Last line will never raise NameError because `error` is only
178+
falsy if the `except:` block has not been entered."""
179+
self.flakes('''
180+
exc = 'Original value'
181+
def func():
182+
global exc
183+
error = None
184+
try:
185+
raise ValueError('ve')
186+
except ValueError as exc:
187+
error = 'exception logged'
188+
if error:
189+
print(error)
190+
else:
191+
exc
192+
''')
193+
25194
def test_functionsNeedGlobalScope(self):
26195
self.flakes('''
27196
class a:

0 commit comments

Comments
 (0)