2
# Copyright (C) 2006, 2007 Michael Bayer mike_mp@zzzcomputing.com
4
# This module is part of Mako and is released under
5
# the MIT License: http://www.opensource.org/licenses/mit-license.php
7
"""utilities for generating and formatting literal Python code."""
10
from StringIO import StringIO
12
class PythonPrinter(object):
13
def __init__(self, stream):
17
# a stack storing information about why we incremented
18
# the indentation counter, to help us determine if we
20
self.indent_detail = []
22
# the string of whitespace multiplied by the indent
23
# counter to produce a line
24
self.indentstring = " "
26
# the stream we are writing to
29
# a list of lines that represents a buffered "block" of code,
30
# which can be later printed relative to an indent level
33
self.in_indent_lines = False
35
self._reset_multi_line_flags()
37
def write(self, text):
38
self.stream.write(text)
40
def write_indented_block(self, block):
41
"""print a line or lines of python which already contain indentation.
43
The indentation of the total block of lines will be adjusted to that of
44
the current indent level."""
45
self.in_indent_lines = False
46
for l in re.split(r'\r?\n', block):
47
self.line_buffer.append(l)
49
def writelines(self, *lines):
50
"""print a series of lines of python."""
54
def writeline(self, line):
55
"""print a line of python, indenting it according to the current indent level.
57
this also adjusts the indentation counter according to the content of the line."""
59
if not self.in_indent_lines:
60
self._flush_adjusted_lines()
61
self.in_indent_lines = True
63
decreased_indent = False
66
re.match(r"^\s*#",line) or
67
re.match(r"^\s*$", line)
73
is_comment = line and len(line) and line[0] == '#'
75
# see if this line should decrease the indentation level
76
if (not decreased_indent and
78
(not hastext or self._is_unindentor(line))
83
# if the indent_detail stack is empty, the user
84
# probably put extra closures - the resulting
85
# module wont compile.
86
if len(self.indent_detail) == 0:
87
raise "Too many whitespace closures"
88
self.indent_detail.pop()
94
self.stream.write(self._indent_line(line) + "\n")
96
# see if this line should increase the indentation level.
97
# note that a line can both decrase (before printing) and
98
# then increase (after printing) the indentation level.
100
if re.search(r":[ \t]*(?:#.*)?$", line):
101
# increment indentation count, and also
102
# keep track of what the keyword was that indented us,
103
# if it is a python compound statement keyword
104
# where we might have to look for an "unindent" keyword
105
match = re.match(r"^\s*(if|try|elif|while|for)", line)
107
# its a "compound" keyword, so we will check for "unindentors"
108
indentor = match.group(1)
110
self.indent_detail.append(indentor)
113
# its not a "compound" keyword. but lets also
114
# test for valid Python keywords that might be indenting us,
115
# else assume its a non-indenting line
116
m2 = re.match(r"^\s*(def|class|else|elif|except|finally)", line)
119
self.indent_detail.append(indentor)
122
"""close this printer, flushing any remaining lines."""
123
self._flush_adjusted_lines()
125
def _is_unindentor(self, line):
126
"""return true if the given line is an 'unindentor', relative to the last 'indent' event received."""
128
# no indentation detail has been pushed on; return False
129
if len(self.indent_detail) == 0:
132
indentor = self.indent_detail[-1]
134
# the last indent keyword we grabbed is not a
135
# compound statement keyword; return False
139
# if the current line doesnt have one of the "unindentor" keywords,
141
match = re.match(r"^\s*(else|elif|except|finally)", line)
145
# whitespace matches up, we have a compound indentor,
146
# and this line has an unindentor, this
147
# is probably good enough
150
# should we decide that its not good enough, heres
151
# more stuff to check.
152
#keyword = match.group(1)
154
# match the original indent keyword
156
# (r'if|elif', r'else|elif'),
157
# (r'try', r'except|finally|else'),
158
# (r'while|for', r'else'),
160
# if re.match(crit[0], indentor) and re.match(crit[1], keyword): return True
164
def _indent_line(self, line, stripspace = ''):
165
"""indent the given line according to the current indent level.
167
stripspace is a string of space that will be truncated from the start of the line
169
return re.sub(r"^%s" % stripspace, self.indentstring * self.indent, line)
171
def _reset_multi_line_flags(self):
172
"""reset the flags which would indicate we are in a backslashed or triple-quoted section."""
173
(self.backslashed, self.triplequoted) = (False, False)
175
def _in_multi_line(self, line):
176
"""return true if the given line is part of a multi-line block, via backslash or triple-quote."""
177
# we are only looking for explicitly joined lines here,
178
# not implicit ones (i.e. brackets, braces etc.). this is just
179
# to guard against the possibility of modifying the space inside
180
# of a literal multiline string with unfortunately placed whitespace
182
current_state = (self.backslashed or self.triplequoted)
184
if re.search(r"\\$", line):
185
self.backslashed = True
187
self.backslashed = False
189
triples = len(re.findall(r"\"\"\"|\'\'\'", line))
190
if triples == 1 or triples % 2 != 0:
191
self.triplequoted = not self.triplequoted
195
def _flush_adjusted_lines(self):
197
self._reset_multi_line_flags()
199
for entry in self.line_buffer:
200
if self._in_multi_line(entry):
201
self.stream.write(entry + "\n")
203
entry = string.expandtabs(entry)
204
if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry):
205
stripspace = re.match(r"^([ \t]*)", entry).group(1)
206
self.stream.write(self._indent_line(entry, stripspace) + "\n")
208
self.line_buffer = []
209
self._reset_multi_line_flags()
212
def adjust_whitespace(text):
213
"""remove the left-whitespace margin of a block of Python code."""
214
state = [False, False]
215
(backslashed, triplequoted) = (0, 1)
216
def in_multi_line(line):
217
current_state = (state[backslashed] or state[triplequoted])
218
if re.search(r"\\$", line):
219
state[backslashed] = True
221
state[backslashed] = False
222
line = re.split(r'#', line)[0]
223
triples = len(re.findall(r"\"\"\"|\'\'\'", line))
224
if triples == 1 or triples % 2 != 0:
225
state[triplequoted] = not state[triplequoted]
228
def _indent_line(line, stripspace = ''):
229
return re.sub(r"^%s" % stripspace, '', line)
234
for line in re.split(r'\r?\n', text):
235
if in_multi_line(line):
236
stream.write(line + "\n")
238
line = string.expandtabs(line)
239
if stripspace is None and re.search(r"^[ \t]*[^# \t]", line):
240
stripspace = re.match(r"^([ \t]*)", line).group(1)
241
stream.write(_indent_line(line, stripspace) + "\n")
242
return stream.getvalue()