Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Process doctest scope directly under the module scope #52

Merged
merged 1 commit into from
Nov 24, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 23 additions & 14 deletions pyflakes/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,11 +254,11 @@ class GeneratorScope(Scope):


class ModuleScope(Scope):
pass
"""Scope for a module."""


class DoctestScope(ModuleScope):
pass
"""Scope for a doctest."""


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

def _in_doctest(self):
return (len(self.scopeStack) >= 2 and
isinstance(self.scopeStack[1], DoctestScope))

@property
def scope(self):
return self.scopeStack[-1]
Expand Down Expand Up @@ -681,6 +685,10 @@ def handleDoctests(self, node):
return
if not examples:
return

# Place doctest in module scope
saved_stack = self.scopeStack
self.scopeStack = [self.scopeStack[0]]
node_offset = self.offset or (0, 0)
self.pushScope(DoctestScope)
underscore_in_builtins = '_' in self.builtIns
Expand All @@ -704,6 +712,7 @@ def handleDoctests(self, node):
if not underscore_in_builtins:
self.builtIns.remove('_')
self.popScope()
self.scopeStack = saved_stack

def ignore(self, node):
pass
Expand Down Expand Up @@ -745,14 +754,8 @@ def GLOBAL(self, node):
"""
Keep track of globals declarations.
"""
for i, scope in enumerate(self.scopeStack):
if isinstance(scope, DoctestScope):
global_scope_index = i
global_scope = scope
break
else:
global_scope_index = 0
global_scope = self.scopeStack[0]
global_scope_index = 1 if self._in_doctest() else 0
global_scope = self.scopeStack[global_scope_index]

# Ignore 'global' statement in global scope.
if self.scope is not global_scope:
Expand Down Expand Up @@ -861,9 +864,11 @@ def FUNCTIONDEF(self, node):
self.handleNode(deco, node)
self.LAMBDA(node)
self.addBinding(node, FunctionDefinition(node.name, node))
# doctest does not process doctest within a doctest
if self.withDoctest and not any(
isinstance(scope, DoctestScope) for scope in self.scopeStack):
# doctest does not process doctest within a doctest,
# or in nested functions.
if (self.withDoctest and
not self._in_doctest() and
not isinstance(self.scope, FunctionScope)):
self.deferFunction(lambda: self.handleDoctests(node))

ASYNCFUNCTIONDEF = FUNCTIONDEF
Expand Down Expand Up @@ -963,7 +968,11 @@ def CLASSDEF(self, node):
for keywordNode in node.keywords:
self.handleNode(keywordNode, node)
self.pushScope(ClassScope)
if self.withDoctest:
# doctest does not process doctest within a doctest
# classes within classes are processed.
if (self.withDoctest and
not self._in_doctest() and
not isinstance(self.scope, FunctionScope)):
self.deferFunction(lambda: self.handleDoctests(node))
for stmt in node.body:
self.handleNode(stmt, node)
Expand Down
48 changes: 48 additions & 0 deletions pyflakes/test/test_doctests.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,54 @@ def doctest_stuff():
'''
""", m.UndefinedName)

def test_nested_class(self):
"""Doctest within nested class are processed."""
self.flakes("""
class C:
class D:
'''
>>> m
'''
def doctest_stuff(self):
'''
>>> m
'''
return 1
""", m.UndefinedName, m.UndefinedName)

def test_ignore_nested_function(self):
"""Doctest module does not process doctest in nested functions."""
# 'syntax error' would cause a SyntaxError if the doctest was processed.
# However doctest does not find doctest in nested functions
# (https://bugs.python.org/issue1650090). If nested functions were
# processed, this use of m should cause UndefinedName, and the
# name inner_function should probably exist in the doctest scope.
self.flakes("""
def doctest_stuff():
def inner_function():
'''
>>> syntax error
>>> inner_function()
1
>>> m
'''
return 1
m = inner_function()
return m
""")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would a simpler test suffice? For example a would-be SyntaxError:

    self.flakes('''
    def doctest_stuff():
        def inner_function():
            """
                >>> if True:
                ... pass
            """
    ''')

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler test would be good. I'd go with a slightly more obvious syntax error, which doesnt use doctest indentation, like syntax error or if: pass. Is there a more brutally obvious syntax error?

I'd prefer to keep the existing use of 'inner_function' and 'm', so that if/when nested functions are processed by doctest, those variables will be re-considered, if only because git blame will point the coder back to this discussion. But I'm happy to remove them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simpler test would be good. I'd go with a slightly more obvious syntax error, which doesnt use doctest indentation, like syntax error or if: pass. Is there a more brutally obvious syntax error?

I like syntax error myself.

I'd prefer to keep the existing use of 'inner_function' and 'm', so that if/when nested functions are processed by doctest, those variables will be re-considered, if only because git blame will point the coder back to this discussion. But I'm happy to remove them.

I don't really care as long as the error is more obvious.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added 'syntax error', and a more detailed comment.


def test_inaccessible_scope_class(self):
"""Doctest may not access class scope."""
self.flakes("""
class C:
def doctest_stuff(self):
'''
>>> m
'''
return 1
m = 1
""", m.UndefinedName)

def test_importBeforeDoctest(self):
self.flakes("""
import foo
Expand Down