Skip to content

Commit 7e86708

Browse files
paulsmithtrotterdylan
authored andcommitted
Implement print_function future feature (google#23)
* Implement print_function future feature This allows the import statement `from __future__ import print_function` to work, by setting a flag on the parser to treat print statement/keyword as a syntax error.
1 parent db97d4f commit 7e86708

11 files changed

+355
-49
lines changed

compiler/block.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class Block(object):
6262
_runtime = None
6363
_strings = None
6464
imports = None
65+
_future_features = None
6566

6667
def __init__(self, parent_block, name):
6768
self.parent_block = parent_block
@@ -104,6 +105,11 @@ def lines(self):
104105
def strings(self):
105106
return self._module_block._strings # pylint: disable=protected-access
106107

108+
@property
109+
def future_features(self):
110+
# pylint: disable=protected-access
111+
return self._module_block._future_features
112+
107113
@abc.abstractmethod
108114
def bind_var(self, writer, name, value):
109115
"""Writes Go statements for assigning value to named var in this block.
@@ -216,7 +222,8 @@ class ModuleBlock(Block):
216222
imports: A dict mapping fully qualified Go package names to Package objects.
217223
"""
218224

219-
def __init__(self, full_package_name, runtime, libroot, filename, lines):
225+
def __init__(self, full_package_name, runtime, libroot, filename, lines,
226+
future_features):
220227
super(ModuleBlock, self).__init__(None, '<module>')
221228
self._full_package_name = full_package_name
222229
self._runtime = runtime
@@ -225,6 +232,7 @@ def __init__(self, full_package_name, runtime, libroot, filename, lines):
225232
self._lines = lines
226233
self._strings = set()
227234
self.imports = {}
235+
self._future_features = future_features
228236

229237
def bind_var(self, writer, name, value):
230238
writer.write_checked_call1(
@@ -440,5 +448,5 @@ def __init__(self, node):
440448
raise util.ParseError(node, msg)
441449
self.vars[name] = Var(name, Var.TYPE_PARAM, arg_index=i)
442450

443-
def visit_Yield(self, unused_node):
451+
def visit_Yield(self, unused_node): # pylint: disable=unused-argument
444452
self.is_generator = True

compiler/block_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121
import unittest
2222

2323
from grumpy.compiler import block
24+
from grumpy.compiler import stmt
2425
from grumpy.compiler import util
2526

26-
2727
class PackageTest(unittest.TestCase):
2828

2929
def testCreate(self):
@@ -243,7 +243,8 @@ def testYieldExpr(self):
243243

244244

245245
def _MakeModuleBlock():
246-
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [])
246+
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [],
247+
stmt.FutureFeatures())
247248

248249

249250
def _ParseStmt(stmt_str):

compiler/expr_visitor_test.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from grumpy.compiler import block
2525
from grumpy.compiler import expr_visitor
2626
from grumpy.compiler import shard_test
27+
from grumpy.compiler import stmt
2728
from grumpy.compiler import util
2829

2930

@@ -220,7 +221,8 @@ def testUnaryOpNotImplemented(self):
220221

221222

222223
def _MakeModuleBlock():
223-
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [])
224+
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [],
225+
stmt.FutureFeatures())
224226

225227

226228
def _ParseExpr(expr):

compiler/stmt.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,103 @@
3232
_nil_expr = expr.nil_expr
3333

3434

35+
# Parser flags, set on 'from __future__ import *', see parser_flags on
36+
# StatementVisitor below. Note these have the same values as CPython.
37+
FUTURE_DIVISION = 0x2000
38+
FUTURE_ABSOLUTE_IMPORT = 0x4000
39+
FUTURE_PRINT_FUNCTION = 0x10000
40+
FUTURE_UNICODE_LITERALS = 0x20000
41+
42+
# Names for future features in 'from __future__ import *'. Map from name in the
43+
# import statement to a tuple of the flag for parser, and whether we've (grumpy)
44+
# implemented the feature yet.
45+
future_features = {
46+
"division": (FUTURE_DIVISION, False),
47+
"absolute_import": (FUTURE_ABSOLUTE_IMPORT, False),
48+
"print_function": (FUTURE_PRINT_FUNCTION, True),
49+
"unicode_literals": (FUTURE_UNICODE_LITERALS, False),
50+
}
51+
52+
# These future features are already in the language proper as of 2.6, so
53+
# importing them via __future__ has no effect.
54+
redundant_future_features = ["generators", "with_statement", "nested_scopes"]
55+
56+
late_future = 'from __future__ imports must occur at the beginning of the file'
57+
58+
59+
def import_from_future(node):
60+
"""Processes a future import statement, returning set of flags it defines."""
61+
assert isinstance(node, ast.ImportFrom)
62+
assert node.module == '__future__'
63+
flags = 0
64+
for alias in node.names:
65+
name = alias.name
66+
if name in future_features:
67+
flag, implemented = future_features[name]
68+
if not implemented:
69+
msg = 'future feature {} not yet implemented by grumpy'.format(name)
70+
raise util.ParseError(node, msg)
71+
flags |= flag
72+
elif name == 'braces':
73+
raise util.ParseError(node, 'not a chance')
74+
elif name not in redundant_future_features:
75+
msg = 'future feature {} is not defined'.format(name)
76+
raise util.ParseError(node, msg)
77+
return flags
78+
79+
80+
class FutureFeatures(object):
81+
def __init__(self):
82+
self.parser_flags = 0
83+
self.future_lineno = 0
84+
85+
86+
def visit_future(node):
87+
"""Accumulates a set of compiler flags for the compiler __future__ imports.
88+
89+
Returns an instance of FutureFeatures which encapsulates the flags and the
90+
line number of the last valid future import parsed. A downstream parser can
91+
use the latter to detect invalid future imports that appear too late in the
92+
file.
93+
"""
94+
# If this is the module node, do an initial pass through the module body's
95+
# statements to detect future imports and process their directives (i.e.,
96+
# set compiler flags), and detect ones that don't appear at the beginning of
97+
# the file. The only things that can proceed a future statement are other
98+
# future statements and/or a doc string.
99+
assert isinstance(node, ast.Module)
100+
ff = FutureFeatures()
101+
done = False
102+
found_docstring = False
103+
for node in node.body:
104+
if isinstance(node, ast.ImportFrom):
105+
modname = node.module
106+
if modname == '__future__':
107+
if done:
108+
raise util.ParseError(node, late_future)
109+
ff.parser_flags |= import_from_future(node)
110+
ff.future_lineno = node.lineno
111+
else:
112+
done = True
113+
elif isinstance(node, ast.Expr) and not found_docstring:
114+
e = node.value
115+
if not isinstance(e, ast.Str): # pylint: disable=simplifiable-if-statement
116+
done = True
117+
else:
118+
found_docstring = True
119+
else:
120+
done = True
121+
return ff
122+
123+
35124
class StatementVisitor(ast.NodeVisitor):
36125
"""Outputs Go statements to a Writer for the given Python nodes."""
37126

38127
# pylint: disable=invalid-name,missing-docstring
39128

40129
def __init__(self, block_):
41130
self.block = block_
131+
self.future_features = self.block.future_features or FutureFeatures()
42132
self.writer = util.Writer()
43133
self.expr_visitor = expr_visitor.ExprVisitor(self.block, self.writer)
44134

@@ -286,6 +376,12 @@ def visit_ImportFrom(self, node):
286376
mod.expr, self.block.intern(name))
287377
self.block.bind_var(
288378
self.writer, alias.asname or alias.name, member.expr)
379+
elif node.module == '__future__':
380+
# At this stage all future imports are done in an initial pass (see
381+
# visit() above), so if they are encountered here after the last valid
382+
# __future__ then it's a syntax error.
383+
if node.lineno > self.future_features.future_lineno:
384+
raise util.ParseError(node, late_future)
289385
else:
290386
# NOTE: Assume that the names being imported are all modules within a
291387
# package. E.g. "from a.b import c" is importing the module c from package
@@ -305,6 +401,8 @@ def visit_Pass(self, node):
305401
self._write_py_context(node.lineno)
306402

307403
def visit_Print(self, node):
404+
if self.future_features.parser_flags & FUTURE_PRINT_FUNCTION:
405+
raise util.ParseError(node, 'syntax error (print is not a keyword)')
308406
self._write_py_context(node.lineno)
309407
with self.block.alloc_temp('[]*πg.Object') as args:
310408
self.writer.write('{} = make([]*πg.Object, {})'.format(

compiler/stmt_test.py

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,105 @@ def testImportNativeType(self):
287287
from __go__.time import type_Duration as Duration
288288
print Duration""")))
289289

290-
def testPrint(self):
290+
def testPrintStatement(self):
291291
self.assertEqual((0, 'abc 123\nfoo bar\n'), _GrumpRun(textwrap.dedent("""\
292292
print 'abc',
293293
print '123'
294294
print 'foo', 'bar'""")))
295295

296+
def testImportFromFuture(self):
297+
testcases = [
298+
('from __future__ import print_function', stmt.FUTURE_PRINT_FUNCTION),
299+
('from __future__ import generators', 0),
300+
('from __future__ import generators, print_function',
301+
stmt.FUTURE_PRINT_FUNCTION),
302+
]
303+
304+
for i, tc in enumerate(testcases):
305+
source, want_flags = tc
306+
mod = ast.parse(textwrap.dedent(source))
307+
node = mod.body[0]
308+
got = stmt.import_from_future(node)
309+
msg = '#{}: want {}, got {}'.format(i, want_flags, got)
310+
self.assertEqual(want_flags, got, msg=msg)
311+
312+
def testImportFromFutureParseError(self):
313+
testcases = [
314+
# NOTE: move this group to testImportFromFuture as they are implemented
315+
# by grumpy
316+
('from __future__ import absolute_import',
317+
r'future feature \w+ not yet implemented'),
318+
('from __future__ import division',
319+
r'future feature \w+ not yet implemented'),
320+
('from __future__ import unicode_literals',
321+
r'future feature \w+ not yet implemented'),
322+
323+
('from __future__ import braces', 'not a chance'),
324+
('from __future__ import nonexistant_feature',
325+
r'future feature \w+ is not defined'),
326+
]
327+
328+
for tc in testcases:
329+
source, want_regexp = tc
330+
mod = ast.parse(source)
331+
node = mod.body[0]
332+
self.assertRaisesRegexp(util.ParseError, want_regexp,
333+
stmt.import_from_future, node)
334+
335+
def testVisitFuture(self):
336+
testcases = [
337+
('from __future__ import print_function',
338+
stmt.FUTURE_PRINT_FUNCTION, 1),
339+
("""\
340+
"module docstring"
341+
342+
from __future__ import print_function
343+
""", stmt.FUTURE_PRINT_FUNCTION, 3),
344+
("""\
345+
"module docstring"
346+
347+
from __future__ import print_function, with_statement
348+
from __future__ import nested_scopes
349+
""", stmt.FUTURE_PRINT_FUNCTION, 4),
350+
]
351+
352+
for tc in testcases:
353+
source, flags, lineno = tc
354+
mod = ast.parse(textwrap.dedent(source))
355+
future_features = stmt.visit_future(mod)
356+
self.assertEqual(future_features.parser_flags, flags)
357+
self.assertEqual(future_features.future_lineno, lineno)
358+
359+
def testVisitFutureParseError(self):
360+
testcases = [
361+
# future after normal imports
362+
"""\
363+
import os
364+
from __future__ import print_function
365+
""",
366+
# future after non-docstring expression
367+
"""
368+
asd = 123
369+
from __future__ import print_function
370+
"""
371+
]
372+
373+
for source in testcases:
374+
mod = ast.parse(textwrap.dedent(source))
375+
self.assertRaisesRegexp(util.ParseError, stmt.late_future,
376+
stmt.visit_future, mod)
377+
378+
def testFutureFeaturePrintFunction(self):
379+
want = "abc\n123\nabc 123\nabcx123\nabc 123 "
380+
self.assertEqual((0, want), _GrumpRun(textwrap.dedent("""\
381+
"module docstring is ok to proceed __future__"
382+
from __future__ import print_function
383+
print('abc')
384+
print(123)
385+
print('abc', 123)
386+
print('abc', 123, sep='x')
387+
print('abc', 123, end=' ')""")))
388+
296389
def testRaiseExitStatus(self):
297390
self.assertEqual(1, _GrumpRun('raise Exception')[0])
298391

@@ -465,14 +558,17 @@ def testWriteExceptDispatcherMultipleExcept(self):
465558

466559

467560
def _MakeModuleBlock():
468-
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [])
561+
return block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>', [],
562+
stmt.FutureFeatures())
469563

470564

471565
def _ParseAndVisit(source):
566+
mod = ast.parse(source)
567+
future_features = stmt.visit_future(mod)
472568
b = block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>',
473-
source.split('\n'))
569+
source.split('\n'), future_features)
474570
visitor = stmt.StatementVisitor(b)
475-
visitor.visit(ast.parse(source))
571+
visitor.visit(mod)
476572
return visitor
477573

478574

compiler/util_test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from grumpy.compiler import block
2222
from grumpy.compiler import util
23+
from grumpy.compiler import stmt
2324

2425

2526
class WriterTest(unittest.TestCase):
@@ -34,8 +35,9 @@ def testIndentBlock(self):
3435

3536
def testWriteBlock(self):
3637
writer = util.Writer()
37-
writer.write_block(block.ModuleBlock(
38-
'__main__', 'grumpy', 'grumpy/lib', '<test>', []), 'BODY')
38+
mod_block = block.ModuleBlock('__main__', 'grumpy', 'grumpy/lib', '<test>',
39+
[], stmt.FutureFeatures())
40+
writer.write_block(mod_block, 'BODY')
3941
output = writer.out.getvalue()
4042
dispatch = 'switch πF.State() {\n\tcase 0:\n\tdefault: panic'
4143
self.assertIn(dispatch, output)

0 commit comments

Comments
 (0)