2 # Copyright (C) 2006, 2007, 2008 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)
217 def in_multi_line(line):
218 start_state = (state[backslashed] or state[triplequoted])
220 if re.search(r"\\$", line):
221 state[backslashed] = True
223 state[backslashed] = False
228 return m, t[len(m.group(0)):]
233 if state[triplequoted]:
234 m, line = match(r"%s" % state[triplequoted], line)
236 state[triplequoted] = False
238 m, line = match(r".*?(?=%s|$)" % state[triplequoted], line)
240 m, line = match(r'#', line)
244 m, line = match(r"\"\"\"|\'\'\'", line)
246 state[triplequoted] = m.group(0)
249 m, line = match(r".*?(?=\"\"\"|\'\'\'|#|$)", line)
253 def _indent_line(line, stripspace = ''):
254 return re.sub(r"^%s" % stripspace, '', line)
259 for line in re.split(r'\r?\n', text):
260 if in_multi_line(line):
263 line = string.expandtabs(line)
264 if stripspace is None and re.search(r"^[ \t]*[^# \t]", line):
265 stripspace = re.match(r"^([ \t]*)", line).group(1)
266 lines.append(_indent_line(line, stripspace))
267 return "\n".join(lines)