Skip to content

Commit 56da281

Browse files
committed
Allow __future__ in doctest
Removes Checker.futuresAllowed and replaces it with ModuleScope._futures_allowed.
1 parent 265766d commit 56da281

File tree

2 files changed

+43
-20
lines changed

2 files changed

+43
-20
lines changed

pyflakes/checker.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,18 @@ def redefines(self, other):
147147
return isinstance(other, Definition) and self.name == other.name
148148

149149

150+
class FutureImportation(Importation):
151+
"""
152+
A binding created by a from `__future__` import statement.
153+
154+
`__future__` imports are implicitly used.
155+
"""
156+
157+
def __init__(self, name, source, scope):
158+
super(FutureImportation, self).__init__(name, source)
159+
self.used = (scope, source)
160+
161+
150162
class Argument(Binding):
151163
"""
152164
Represents binding a name as an argument.
@@ -243,11 +255,12 @@ class GeneratorScope(Scope):
243255

244256

245257
class ModuleScope(Scope):
246-
pass
258+
"""Scope for a module."""
259+
_futures_allowed = True
247260

248261

249262
class DoctestScope(ModuleScope):
250-
pass
263+
"""Scope for a doctest."""
251264

252265

253266
# Globally defined names which are not attributes of the builtins module, or
@@ -299,7 +312,6 @@ def __init__(self, tree, filename='(none)', builtins=None,
299312
self.withDoctest = withDoctest
300313
self.scopeStack = [ModuleScope()]
301314
self.exceptHandlers = [()]
302-
self.futuresAllowed = True
303315
self.root = tree
304316
self.handleChildren(tree)
305317
self.runDeferred(self._deferredFunctions)
@@ -616,9 +628,13 @@ def handleNode(self, node, parent):
616628
node.col_offset += self.offset[1]
617629
if self.traceTree:
618630
print(' ' * self.nodeDepth + node.__class__.__name__)
619-
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
620-
self.isDocstring(node)):
621-
self.futuresAllowed = False
631+
632+
if (isinstance(self.scope, ModuleScope) and
633+
self.scope._futures_allowed and
634+
not (isinstance(node, ast.ImportFrom) or
635+
self.isDocstring(node))):
636+
self.scope._futures_allowed = False
637+
622638
self.nodeDepth += 1
623639
node.depth = self.nodeDepth
624640
node.parent = parent
@@ -964,14 +980,26 @@ def IMPORT(self, node):
964980

965981
def IMPORTFROM(self, node):
966982
if node.module == '__future__':
967-
if not self.futuresAllowed:
983+
# the scope can not have any other type of binding
984+
has_other_import_from = any(
985+
not isinstance(binding, FutureImportation)
986+
for binding in self.scope.values())
987+
988+
# __future__ can only appear at top of each module/doctest scope
989+
# handleNode disables _futures_allowed for any other type
990+
if (not isinstance(self.scope, ModuleScope) or
991+
not self.scope._futures_allowed
992+
or has_other_import_from):
993+
self.scope._futures_allowed = False
968994
self.report(messages.LateFutureImport,
969995
node, [n.name for n in node.names])
970-
else:
971-
self.futuresAllowed = False
996+
return
972997

973998
for alias in node.names:
974-
if alias.name == '*':
999+
name = alias.asname or alias.name
1000+
if node.module == '__future__':
1001+
importation = FutureImportation(name, node, self.scope)
1002+
elif alias.name == '*':
9751003
# Only Python 2, local import * is a SyntaxWarning
9761004
if not PY2 and not isinstance(self.scope, ModuleScope):
9771005
self.report(messages.ImportStarNotPermitted,
@@ -980,10 +1008,8 @@ def IMPORTFROM(self, node):
9801008
self.scope.importStarred = True
9811009
self.report(messages.ImportStarUsed, node, node.module)
9821010
continue
983-
name = alias.asname or alias.name
984-
importation = Importation(name, node)
985-
if node.module == '__future__':
986-
importation.used = (self.scope, node)
1011+
else:
1012+
importation = Importation(name, node)
9871013
self.addBinding(node, importation)
9881014

9891015
def TRY(self, node):

pyflakes/test/test_doctests.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -383,16 +383,13 @@ def func():
383383

384384

385385
class TestOther(_DoctestMixin, TestOther):
386+
"""Run TestOther with each test wrapped in a doctest."""
386387
pass
387388

388389

389390
class TestImports(_DoctestMixin, TestImports):
390-
391-
def test_futureImport(self):
392-
"""XXX This test can't work in a doctest"""
393-
394-
def test_futureImportUsed(self):
395-
"""XXX This test can't work in a doctest"""
391+
"""Run TestImports with each test wrapped in a doctest."""
392+
pass
396393

397394

398395
class TestUndefinedNames(_DoctestMixin, TestUndefinedNames):

0 commit comments

Comments
 (0)