Skip to content

Commit 48db10f

Browse files
committed
Allow __future__ in doctest
Deprecate Checker.futuresAllowed as it is now unused, and is typically unnecessary. The deprecated futuresAllowed refers only to the module scope. Add test_checker to ensure backwards compatibility for Checker.futuresAllowed.
1 parent f048360 commit 48db10f

File tree

4 files changed

+148
-21
lines changed

4 files changed

+148
-21
lines changed

pyflakes/checker.py

Lines changed: 52 additions & 14 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,20 @@ def __init__(self, tree, filename='(none)', builtins=None,
308322
self.popScope()
309323
self.checkDeadScopes()
310324

325+
@property
326+
def futuresAllowed(self):
327+
"""Return whether `__future__` are permitted in the module context."""
328+
warn('Checker.futuresAllowed is deprecated', DeprecationWarning, 2)
329+
if not isinstance(self.scope, ModuleScope):
330+
return False
331+
return self.scopeStack[0]._futures_allowed
332+
333+
@futuresAllowed.setter
334+
def futuresAllowed(self, value):
335+
"""Disable permitting `__future__` in the module."""
336+
warn('Checker.futuresAllowed is deprecated', DeprecationWarning, 2)
337+
self.scopeStack[0]._futures_allowed = value
338+
311339
def deferFunction(self, callable):
312340
"""
313341
Schedule a function handler to be called just before completion.
@@ -606,9 +634,13 @@ def handleNode(self, node, parent):
606634
node.col_offset += self.offset[1]
607635
if self.traceTree:
608636
print(' ' * self.nodeDepth + node.__class__.__name__)
609-
if self.futuresAllowed and not (isinstance(node, ast.ImportFrom) or
610-
self.isDocstring(node)):
611-
self.futuresAllowed = False
637+
638+
if (isinstance(self.scope, ModuleScope) and
639+
self.scope._futures_allowed and
640+
not (isinstance(node, ast.ImportFrom) or
641+
self.isDocstring(node))):
642+
self.scope._futures_allowed = False
643+
612644
self.nodeDepth += 1
613645
node.depth = self.nodeDepth
614646
node.parent = parent
@@ -952,21 +984,27 @@ def IMPORT(self, node):
952984

953985
def IMPORTFROM(self, node):
954986
if node.module == '__future__':
955-
if not self.futuresAllowed:
987+
# __future__ can only appear in module/doctest scope and
988+
# should not have been disabled already in handleNode and
989+
# the scope must not have any other type of binding.
990+
if (not isinstance(self.scope, ModuleScope) or
991+
not self.scope._futures_allowed or
992+
any(not isinstance(binding, FutureImportation)
993+
for binding in self.scope.values())):
994+
self.scope._futures_allowed = False
956995
self.report(messages.LateFutureImport,
957996
node, [n.name for n in node.names])
958-
else:
959-
self.futuresAllowed = False
960997

961998
for alias in node.names:
962-
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 == '*':
9631003
self.scope.importStarred = True
9641004
self.report(messages.ImportStarUsed, node, node.module)
9651005
continue
966-
name = alias.asname or alias.name
967-
importation = Importation(name, node)
968-
if node.module == '__future__':
969-
importation.used = (self.scope, node)
1006+
else:
1007+
importation = Importation(name, node)
9701008
self.addBinding(node, importation)
9711009

9721010
def TRY(self, node):

pyflakes/test/harness.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@
1919
class TestCase(unittest.TestCase):
2020

2121
withDoctest = False
22+
checker_cls = checker.Checker
2223

2324
def flakes(self, input, *expectedOutputs, **kw):
2425
tree = compile(textwrap.dedent(input), "<test>", "exec", PyCF_ONLY_AST)
25-
w = checker.Checker(tree, withDoctest=self.withDoctest, **kw)
26+
if 'withDoctest' not in kw:
27+
kw['withDoctest'] = self.withDoctest
28+
29+
w = self.checker_cls(tree, **kw)
2630
outputs = [type(o) for o in w.messages]
2731
expectedOutputs = list(expectedOutputs)
2832
outputs.sort(key=lambda t: t.__name__)

pyflakes/test/test_checker.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""Tests for L{pyflakes.checker}."""
2+
3+
from pyflakes.checker import Checker, getNodeName
4+
from pyflakes.test.harness import TestCase
5+
6+
7+
class StoredKwargsChecker(Checker):
8+
9+
"""Checker that stores keyword arguments for use in tests."""
10+
11+
def __init__(self, tree, withDoctest, **kwargs):
12+
self._test_result = None
13+
if 'test_result' in kwargs:
14+
self._test_result = kwargs['test_result']
15+
del kwargs['test_result']
16+
self._test_kwargs = kwargs
17+
super(StoredKwargsChecker, self).__init__(
18+
tree, withDoctest=withDoctest)
19+
20+
21+
class DeprecatedFuturesAllowedChecker(StoredKwargsChecker):
22+
23+
"""
24+
Checker that ignores tree and injects a deferred function.
25+
26+
Args:
27+
capture_at: node to activate state capture
28+
enable_at: node to force enabling of futures
29+
"""
30+
31+
def NAME(self, node):
32+
name = getNodeName(node)
33+
if self._test_result is None:
34+
if name == self._test_kwargs['capture_at']:
35+
self._test_result = self.futuresAllowed
36+
super(DeprecatedFuturesAllowedChecker, self).NAME(node)
37+
38+
def IMPORTFROM(self, node):
39+
name = node.module
40+
if name == self._test_kwargs.get('enable_at'):
41+
self.futuresAllowed = True
42+
43+
if self._test_result is None:
44+
if name == self._test_kwargs['capture_at']:
45+
self._test_result = self.futuresAllowed
46+
super(DeprecatedFuturesAllowedChecker, self).IMPORTFROM(node)
47+
48+
def STR(self, node):
49+
name = node.s
50+
if self._test_result is None:
51+
if name == self._test_kwargs['capture_at']:
52+
self._test_result = self.futuresAllowed
53+
54+
55+
class TestFuturesAllowed(TestCase):
56+
"""Tests for L{Checker} futuresAllowed flag."""
57+
58+
checker_cls = DeprecatedFuturesAllowedChecker
59+
60+
def test_future_at_beginning(self):
61+
"""The futuresAllowed enabled initially."""
62+
checker = self.flakes('''
63+
"""docstring allowed."""
64+
from __future__ import print_function
65+
''', capture_at='__future__')
66+
67+
assert checker._test_result is True
68+
69+
def test_future_disabled_after_node(self):
70+
"""The futuresAllowed disabled after node."""
71+
checker = self.flakes('1\nf = 1', capture_at='f')
72+
73+
assert hasattr(checker, '_test_result')
74+
assert checker._test_result is False
75+
76+
checker = self.flakes('def f(): a = 1; return a', capture_at='a')
77+
78+
assert checker._test_result is False
79+
80+
def test_future_set_allowed(self):
81+
"""Set futuresAllowed permitted."""
82+
checker = self.flakes('''
83+
1
84+
from __future__ import print_function
85+
a = 1
86+
''', enable_at='__future__', capture_at='a')
87+
88+
assert checker._test_result is False

pyflakes/test/test_doctests.py

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

364364

365365
class TestOther(_DoctestMixin, TestOther):
366+
"""Run TestOther with each test wrapped in a doctest."""
366367
pass
367368

368369

369370
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"""
371+
"""Run TestImports with each test wrapped in a doctest."""
372+
pass
376373

377374

378375
class TestUndefinedNames(_DoctestMixin, TestUndefinedNames):

0 commit comments

Comments
 (0)