Skip to content

Commit 2976f24

Browse files
committed
Allow __future__ in doctest
Deprecate Checker.futuresAllowed as it is now unused, and is typically unnecessary.
1 parent d6de471 commit 2976f24

File tree

2 files changed

+56
-17
lines changed

2 files changed

+56
-17
lines changed

pyflakes/checker.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import os
99
import sys
1010

11+
from warnings import warn
12+
1113
PY2 = sys.version_info < (3, 0)
1214
PY32 = sys.version_info < (3, 3) # Python 2.5 to 3.2
1315
PY33 = sys.version_info < (3, 4) # Python 2.5 to 3.3
@@ -141,6 +143,18 @@ def redefines(self, other):
141143
return isinstance(other, Definition) and self.name == other.name
142144

143145

146+
class FutureImportation(Importation):
147+
"""
148+
A binding created by a from `__future__` import statement.
149+
150+
`__future__` imports are implicitly used.
151+
"""
152+
153+
def __init__(self, name, source, scope):
154+
super(FutureImportation, self).__init__(name, source)
155+
self.used = (scope, source)
156+
157+
144158
class Argument(Binding):
145159
"""
146160
Represents binding a name as an argument.
@@ -237,11 +251,12 @@ class GeneratorScope(Scope):
237251

238252

239253
class ModuleScope(Scope):
240-
pass
254+
"""Scope for a module."""
255+
_futures_allowed = True
241256

242257

243258
class DoctestScope(ModuleScope):
244-
pass
259+
"""Scope for a doctest."""
245260

246261

247262
# Globally defined names which are not attributes of the builtins module, or
@@ -293,7 +308,6 @@ def __init__(self, tree, filename='(none)', builtins=None,
293308
self.withDoctest = withDoctest
294309
self.scopeStack = [ModuleScope()]
295310
self.exceptHandlers = [()]
296-
self.futuresAllowed = True
297311
self.root = tree
298312
self.handleChildren(tree)
299313
self.runDeferred(self._deferredFunctions)
@@ -308,6 +322,26 @@ def __init__(self, tree, filename='(none)', builtins=None,
308322
self.popScope()
309323
self.checkDeadScopes()
310324

325+
@property
326+
def _module_scopes(self):
327+
"""Return list of module scope and doctest scope if it exists."""
328+
return [scope for scope in self.scopeStack
329+
if isinstance(scope, ModuleScope)]
330+
331+
@property
332+
def futuresAllowed(self):
333+
"""Return whether `__future__` are permitted in the current context."""
334+
warn('Checker.futuresAllowed is deprecated', DeprecationWarning, 2)
335+
if not isinstance(self.scope, ModuleScope):
336+
return False
337+
return self.scope._futures_allowed
338+
339+
@futuresAllowed.setter
340+
def futuresAllowed(self, value):
341+
"""Disable permitting `__future__` in the current context."""
342+
self.futuresAllowed() # trigger deprecation warning
343+
self._module_scopes[-1]._futures_allowed = value
344+
311345
def deferFunction(self, callable):
312346
"""
313347
Schedule a function handler to be called just before completion.
@@ -602,9 +636,13 @@ def handleNode(self, node, parent):
602636
node.col_offset += self.offset[1]
603637
if self.traceTree:
604638
print(' ' * self.nodeDepth + node.__class__.__name__)
605-
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
606-
self.isDocstring(node)):
607-
self.futuresAllowed = False
639+
640+
if (isinstance(self.scope, ModuleScope) and
641+
self.scope._futures_allowed and
642+
not (isinstance(node, ast.ImportFrom) or
643+
self.isDocstring(node))):
644+
self.scope._futures_allowed = False
645+
608646
self.nodeDepth += 1
609647
node.depth = self.nodeDepth
610648
node.parent = parent
@@ -948,21 +986,27 @@ def IMPORT(self, node):
948986

949987
def IMPORTFROM(self, node):
950988
if node.module == '__future__':
951-
if not self.futuresAllowed:
989+
# __future__ can only appear in module/doctest scope and
990+
# should not have been disabled already in handleNode and
991+
# the scope must not have any other type of binding.
992+
if (not isinstance(self.scope, ModuleScope) or
993+
not self.scope._futures_allowed or
994+
any(not isinstance(binding, FutureImportation)
995+
for binding in self.scope.values())):
996+
self.scope._futures_allowed = False
952997
self.report(messages.LateFutureImport,
953998
node, [n.name for n in node.names])
954-
else:
955-
self.futuresAllowed = False
956999

9571000
for alias in node.names:
9581001
if alias.name == '*':
9591002
self.scope.importStarred = True
9601003
self.report(messages.ImportStarUsed, node, node.module)
9611004
continue
9621005
name = alias.asname or alias.name
963-
importation = Importation(name, node)
9641006
if node.module == '__future__':
965-
importation.used = (self.scope, node)
1007+
importation = FutureImportation(name, node, self.scope)
1008+
else:
1009+
importation = Importation(name, node)
9661010
self.addBinding(node, importation)
9671011

9681012
def TRY(self, node):

pyflakes/test/test_doctests.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,12 +367,7 @@ class TestOther(_DoctestMixin, TestOther):
367367

368368

369369
class TestImports(_DoctestMixin, TestImports):
370-
371-
def test_futureImport(self):
372-
"""XXX This test can't work in a doctest"""
373-
374-
def test_futureImportUsed(self):
375-
"""XXX This test can't work in a doctest"""
370+
pass
376371

377372

378373
class TestUndefinedNames(_DoctestMixin, TestUndefinedNames):

0 commit comments

Comments
 (0)