Skip to content

Commit 93aa3c4

Browse files
jayvdbbitglue
authored andcommitted
Add DoctestScope
Fix bug in 03ffc76 caused by determining the doctest global scope level based on whether parsing doctests was enabled. Also do not parse docstrings within doctests.
1 parent 4feb31f commit 93aa3c4

File tree

3 files changed

+187
-6
lines changed

3 files changed

+187
-6
lines changed

pyflakes/checker.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ class ModuleScope(Scope):
240240
pass
241241

242242

243+
class DoctestScope(ModuleScope):
244+
pass
245+
246+
243247
# Globally defined names which are not attributes of the builtins module, or
244248
# are only present on some platforms.
245249
_MAGIC_GLOBALS = ['__file__', '__builtins__', 'WindowsError']
@@ -625,7 +629,7 @@ def handleDoctests(self, node):
625629
if not examples:
626630
return
627631
node_offset = self.offset or (0, 0)
628-
self.pushScope()
632+
self.pushScope(DoctestScope)
629633
underscore_in_builtins = '_' in self.builtIns
630634
if not underscore_in_builtins:
631635
self.builtIns.add('_')
@@ -681,9 +685,14 @@ def GLOBAL(self, node):
681685
"""
682686
Keep track of globals declarations.
683687
"""
684-
# In doctests, the global scope is an anonymous function at index 1.
685-
global_scope_index = 1 if self.withDoctest else 0
686-
global_scope = self.scopeStack[global_scope_index]
688+
for i, scope in enumerate(self.scopeStack):
689+
if isinstance(scope, DoctestScope):
690+
global_scope_index = i
691+
global_scope = scope
692+
break
693+
else:
694+
global_scope_index = 0
695+
global_scope = self.scopeStack[0]
687696

688697
# Ignore 'global' statement in global scope.
689698
if self.scope is not global_scope:
@@ -763,7 +772,9 @@ def FUNCTIONDEF(self, node):
763772
self.handleNode(deco, node)
764773
self.LAMBDA(node)
765774
self.addBinding(node, FunctionDefinition(node.name, node))
766-
if self.withDoctest:
775+
# doctest does not process doctest within a doctest
776+
if self.withDoctest and not any(
777+
isinstance(scope, DoctestScope) for scope in self.scopeStack):
767778
self.deferFunction(lambda: self.handleDoctests(node))
768779

769780
ASYNCFUNCTIONDEF = FUNCTIONDEF

pyflakes/test/harness.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,37 @@ def flakes(self, input, *expectedOutputs, **kw):
3636
%s''' % (input, expectedOutputs, '\n'.join([str(o) for o in w.messages])))
3737
return w
3838

39-
if sys.version_info < (2, 7):
39+
if not hasattr(unittest.TestCase, 'assertIs'):
4040

4141
def assertIs(self, expr1, expr2, msg=None):
4242
if expr1 is not expr2:
4343
self.fail(msg or '%r is not %r' % (expr1, expr2))
44+
45+
if not hasattr(unittest.TestCase, 'assertIsInstance'):
46+
47+
def assertIsInstance(self, obj, cls, msg=None):
48+
"""Same as self.assertTrue(isinstance(obj, cls))."""
49+
if not isinstance(obj, cls):
50+
self.fail(msg or '%r is not an instance of %r' % (obj, cls))
51+
52+
if not hasattr(unittest.TestCase, 'assertNotIsInstance'):
53+
54+
def assertNotIsInstance(self, obj, cls, msg=None):
55+
"""Same as self.assertFalse(isinstance(obj, cls))."""
56+
if isinstance(obj, cls):
57+
self.fail(msg or '%r is an instance of %r' % (obj, cls))
58+
59+
if not hasattr(unittest.TestCase, 'assertIn'):
60+
61+
def assertIn(self, member, container, msg=None):
62+
"""Just like self.assertTrue(a in b)."""
63+
if member not in container:
64+
self.fail(msg or '%r not found in %r' % (member, container))
65+
66+
if not hasattr(unittest.TestCase, 'assertNotIn'):
67+
68+
def assertNotIn(self, member, container, msg=None):
69+
"""Just like self.assertTrue(a not in b)."""
70+
if member in container:
71+
self.fail(msg or
72+
'%r unexpectedly found in %r' % (member, container))

pyflakes/test/test_doctests.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import textwrap
22

33
from pyflakes import messages as m
4+
from pyflakes.checker import (
5+
DoctestScope,
6+
FunctionScope,
7+
ModuleScope,
8+
)
49
from pyflakes.test.test_other import Test as TestOther
510
from pyflakes.test.test_imports import Test as TestImports
611
from pyflakes.test.test_undefined_names import Test as TestUndefinedNames
@@ -42,6 +47,142 @@ class Test(TestCase):
4247

4348
withDoctest = True
4449

50+
def test_scope_class(self):
51+
"""Check that a doctest is given a DoctestScope."""
52+
checker = self.flakes("""
53+
m = None
54+
55+
def doctest_stuff():
56+
'''
57+
>>> d = doctest_stuff()
58+
'''
59+
f = m
60+
return f
61+
""")
62+
63+
scopes = checker.deadScopes
64+
module_scopes = [
65+
scope for scope in scopes if scope.__class__ is ModuleScope]
66+
doctest_scopes = [
67+
scope for scope in scopes if scope.__class__ is DoctestScope]
68+
function_scopes = [
69+
scope for scope in scopes if scope.__class__ is FunctionScope]
70+
71+
self.assertEqual(len(module_scopes), 1)
72+
self.assertEqual(len(doctest_scopes), 1)
73+
74+
module_scope = module_scopes[0]
75+
doctest_scope = doctest_scopes[0]
76+
77+
self.assertIsInstance(doctest_scope, DoctestScope)
78+
self.assertIsInstance(doctest_scope, ModuleScope)
79+
self.assertNotIsInstance(doctest_scope, FunctionScope)
80+
self.assertNotIsInstance(module_scope, DoctestScope)
81+
82+
self.assertIn('m', module_scope)
83+
self.assertIn('doctest_stuff', module_scope)
84+
85+
self.assertIn('d', doctest_scope)
86+
87+
self.assertEqual(len(function_scopes), 1)
88+
self.assertIn('f', function_scopes[0])
89+
90+
def test_nested_doctest_ignored(self):
91+
"""Check that nested doctests are ignored."""
92+
checker = self.flakes("""
93+
m = None
94+
95+
def doctest_stuff():
96+
'''
97+
>>> def function_in_doctest():
98+
... \"\"\"
99+
... >>> ignored_undefined_name
100+
... \"\"\"
101+
... df = m
102+
... return df
103+
...
104+
>>> function_in_doctest()
105+
'''
106+
f = m
107+
return f
108+
""")
109+
110+
scopes = checker.deadScopes
111+
module_scopes = [
112+
scope for scope in scopes if scope.__class__ is ModuleScope]
113+
doctest_scopes = [
114+
scope for scope in scopes if scope.__class__ is DoctestScope]
115+
function_scopes = [
116+
scope for scope in scopes if scope.__class__ is FunctionScope]
117+
118+
self.assertEqual(len(module_scopes), 1)
119+
self.assertEqual(len(doctest_scopes), 1)
120+
121+
module_scope = module_scopes[0]
122+
doctest_scope = doctest_scopes[0]
123+
124+
self.assertIn('m', module_scope)
125+
self.assertIn('doctest_stuff', module_scope)
126+
self.assertIn('function_in_doctest', doctest_scope)
127+
128+
self.assertEqual(len(function_scopes), 2)
129+
130+
self.assertIn('f', function_scopes[0])
131+
self.assertIn('df', function_scopes[1])
132+
133+
def test_global_module_scope_pollution(self):
134+
"""Check that global in doctest does not pollute module scope."""
135+
checker = self.flakes("""
136+
def doctest_stuff():
137+
'''
138+
>>> def function_in_doctest():
139+
... global m
140+
... m = 50
141+
... df = 10
142+
... m = df
143+
...
144+
>>> function_in_doctest()
145+
'''
146+
f = 10
147+
return f
148+
149+
""")
150+
151+
scopes = checker.deadScopes
152+
module_scopes = [
153+
scope for scope in scopes if scope.__class__ is ModuleScope]
154+
doctest_scopes = [
155+
scope for scope in scopes if scope.__class__ is DoctestScope]
156+
function_scopes = [
157+
scope for scope in scopes if scope.__class__ is FunctionScope]
158+
159+
self.assertEqual(len(module_scopes), 1)
160+
self.assertEqual(len(doctest_scopes), 1)
161+
162+
module_scope = module_scopes[0]
163+
doctest_scope = doctest_scopes[0]
164+
165+
self.assertIn('doctest_stuff', module_scope)
166+
self.assertIn('function_in_doctest', doctest_scope)
167+
168+
self.assertEqual(len(function_scopes), 2)
169+
170+
self.assertIn('f', function_scopes[0])
171+
self.assertIn('df', function_scopes[1])
172+
self.assertIn('m', function_scopes[1])
173+
174+
self.assertNotIn('m', module_scope)
175+
176+
def test_global_undefined(self):
177+
self.flakes("""
178+
global m
179+
180+
def doctest_stuff():
181+
'''
182+
>>> m
183+
'''
184+
""", m.UndefinedName)
185+
45186
def test_importBeforeDoctest(self):
46187
self.flakes("""
47188
import foo

0 commit comments

Comments
 (0)