Skip to content

Commit 6734c37

Browse files
committed
Process doctest scope directly under the module scope
Explicitly place the doctest directly under the module scope, instead of processing it while within the scope where the docstring was encountered. Also do not process doctest which are not processed by default according to the doctest documentation. The primary benefit of moving the doctest scope directly under the module scope is that it may be efficiently identified, as it may only appear second in the stack. However it also ensures that the doctest scope can not access names in scopes between the module scope and the object where the docstring was encountered. As it was, class scope rules prevented the doctest scope from accessing names in a class scope, however the doctest scope was able to access names in an outer function, if the doctest appeared in a nested function. Note that there was no real bug there, as the doctest module does not process doctest in nested functions (Python issue #1650090), so doctest appearing in nested functions are informational only. pyflakes previously inspected doctest in nested functions, and now these are ignored.
1 parent 0532189 commit 6734c37

File tree

2 files changed

+71
-14
lines changed

2 files changed

+71
-14
lines changed

pyflakes/checker.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -254,11 +254,11 @@ class GeneratorScope(Scope):
254254

255255

256256
class ModuleScope(Scope):
257-
pass
257+
"""Scope for a module."""
258258

259259

260260
class DoctestScope(ModuleScope):
261-
pass
261+
"""Scope for a doctest."""
262262

263263

264264
# Globally defined names which are not attributes of the builtins module, or
@@ -352,6 +352,10 @@ def runDeferred(self, deferred):
352352
self.offset = offset
353353
handler()
354354

355+
def _in_doctest(self):
356+
return (len(self.scopeStack) >= 2 and
357+
isinstance(self.scopeStack[1], DoctestScope))
358+
355359
@property
356360
def scope(self):
357361
return self.scopeStack[-1]
@@ -681,6 +685,10 @@ def handleDoctests(self, node):
681685
return
682686
if not examples:
683687
return
688+
689+
# Place doctest in module scope
690+
saved_stack = self.scopeStack
691+
self.scopeStack = [self.scopeStack[0]]
684692
node_offset = self.offset or (0, 0)
685693
self.pushScope(DoctestScope)
686694
underscore_in_builtins = '_' in self.builtIns
@@ -704,6 +712,7 @@ def handleDoctests(self, node):
704712
if not underscore_in_builtins:
705713
self.builtIns.remove('_')
706714
self.popScope()
715+
self.scopeStack = saved_stack
707716

708717
def ignore(self, node):
709718
pass
@@ -745,14 +754,8 @@ def GLOBAL(self, node):
745754
"""
746755
Keep track of globals declarations.
747756
"""
748-
for i, scope in enumerate(self.scopeStack):
749-
if isinstance(scope, DoctestScope):
750-
global_scope_index = i
751-
global_scope = scope
752-
break
753-
else:
754-
global_scope_index = 0
755-
global_scope = self.scopeStack[0]
757+
global_scope_index = 1 if self._in_doctest() else 0
758+
global_scope = self.scopeStack[global_scope_index]
756759

757760
# Ignore 'global' statement in global scope.
758761
if self.scope is not global_scope:
@@ -861,9 +864,11 @@ def FUNCTIONDEF(self, node):
861864
self.handleNode(deco, node)
862865
self.LAMBDA(node)
863866
self.addBinding(node, FunctionDefinition(node.name, node))
864-
# doctest does not process doctest within a doctest
865-
if self.withDoctest and not any(
866-
isinstance(scope, DoctestScope) for scope in self.scopeStack):
867+
# doctest does not process doctest within a doctest,
868+
# or in nested functions.
869+
if (self.withDoctest and
870+
not self._in_doctest() and
871+
not isinstance(self.scope, FunctionScope)):
867872
self.deferFunction(lambda: self.handleDoctests(node))
868873

869874
ASYNCFUNCTIONDEF = FUNCTIONDEF
@@ -963,7 +968,11 @@ def CLASSDEF(self, node):
963968
for keywordNode in node.keywords:
964969
self.handleNode(keywordNode, node)
965970
self.pushScope(ClassScope)
966-
if self.withDoctest:
971+
# doctest does not process doctest within a doctest
972+
# classes within classes are processed.
973+
if (self.withDoctest and
974+
not self._in_doctest() and
975+
not isinstance(self.scope, FunctionScope)):
967976
self.deferFunction(lambda: self.handleDoctests(node))
968977
for stmt in node.body:
969978
self.handleNode(stmt, node)

pyflakes/test/test_doctests.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,54 @@ def doctest_stuff():
190190
'''
191191
""", m.UndefinedName)
192192

193+
def test_nested_class(self):
194+
"""Doctest within nested class are processed."""
195+
self.flakes("""
196+
class C:
197+
class D:
198+
'''
199+
>>> m
200+
'''
201+
def doctest_stuff(self):
202+
'''
203+
>>> m
204+
'''
205+
return 1
206+
""", m.UndefinedName, m.UndefinedName)
207+
208+
def test_ignore_nested_function(self):
209+
"""Doctest module does not process doctest in nested functions."""
210+
# 'syntax error' would cause a SyntaxError if the doctest was processed.
211+
# However doctest does not find doctest in nested functions
212+
# (https://bugs.python.org/issue1650090). If nested functions were
213+
# processed, this use of m should cause UndefinedName, and the
214+
# name inner_function should probably exist in the doctest scope.
215+
self.flakes("""
216+
def doctest_stuff():
217+
def inner_function():
218+
'''
219+
>>> syntax error
220+
>>> inner_function()
221+
1
222+
>>> m
223+
'''
224+
return 1
225+
m = inner_function()
226+
return m
227+
""")
228+
229+
def test_inaccessible_scope_class(self):
230+
"""Doctest may not access class scope."""
231+
self.flakes("""
232+
class C:
233+
def doctest_stuff(self):
234+
'''
235+
>>> m
236+
'''
237+
return 1
238+
m = 1
239+
""", m.UndefinedName)
240+
193241
def test_importBeforeDoctest(self):
194242
self.flakes("""
195243
import foo

0 commit comments

Comments
 (0)