Skip to content

Commit 9ec74d5

Browse files
committed
Allow __future__ in doctest
Deprecate Checker.futuresAllowed as it is now unused, and is typically unnecessary. Add test_checker to ensure backwards compatibility for Checker.futuresAllowed.
1 parent d6de471 commit 9ec74d5

File tree

4 files changed

+184
-18
lines changed

4 files changed

+184
-18
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 tuple of module scope and doctest scope if it exists."""
328+
return tuple(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+
warn('Checker.futuresAllowed is deprecated', DeprecationWarning, 2)
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/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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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_doctest_scope(self):
81+
"""The futuresAllowed is reset for doctest."""
82+
checker = self.flakes('''
83+
1
84+
def f():
85+
"""
86+
>>> from __future__ import print_function
87+
"""
88+
''', withDoctest=True, capture_at='__future__')
89+
90+
assert checker._test_result is True
91+
92+
checker = self.flakes('''
93+
1
94+
def f():
95+
"""
96+
>>> from __future__ import print_function
97+
>>> a = 1
98+
"""
99+
''', withDoctest=True, capture_at='a')
100+
101+
assert checker._test_result is False
102+
103+
def test_future_set_allowed(self):
104+
"""Set futuresAllowed permitted."""
105+
checker = self.flakes('''
106+
1
107+
from __future__ import print_function
108+
a = 1
109+
''', enable_at='__future__', capture_at='a')
110+
111+
assert checker._test_result is False
112+
113+
def test_future_doctest_set_allowed(self):
114+
"""Set futuresAllowed permitted in doctest."""
115+
checker = self.flakes('''
116+
def f():
117+
"""
118+
>>> from __future__ import print_function
119+
>>> 'a'
120+
"""
121+
''', withDoctest=True, enable_at='__future__', capture_at='a')
122+
123+
assert checker._test_result is True

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)