1# mako/pygen.py 2# Copyright (C) 2006-2015 the Mako authors and contributors <see AUTHORS file> 3# 4# This module is part of Mako and is released under 5# the MIT License: http://www.opensource.org/licenses/mit-license.php 6 7"""utilities for generating and formatting literal Python code.""" 8 9import re 10from mako import exceptions 11 12class PythonPrinter(object): 13 def __init__(self, stream): 14 # indentation counter 15 self.indent = 0 16 17 # a stack storing information about why we incremented 18 # the indentation counter, to help us determine if we 19 # should decrement it 20 self.indent_detail = [] 21 22 # the string of whitespace multiplied by the indent 23 # counter to produce a line 24 self.indentstring = " " 25 26 # the stream we are writing to 27 self.stream = stream 28 29 # current line number 30 self.lineno = 1 31 32 # a list of lines that represents a buffered "block" of code, 33 # which can be later printed relative to an indent level 34 self.line_buffer = [] 35 36 self.in_indent_lines = False 37 38 self._reset_multi_line_flags() 39 40 # mapping of generated python lines to template 41 # source lines 42 self.source_map = {} 43 44 def _update_lineno(self, num): 45 self.lineno += num 46 47 def start_source(self, lineno): 48 if self.lineno not in self.source_map: 49 self.source_map[self.lineno] = lineno 50 51 def write_blanks(self, num): 52 self.stream.write("\n" * num) 53 self._update_lineno(num) 54 55 def write_indented_block(self, block): 56 """print a line or lines of python which already contain indentation. 57 58 The indentation of the total block of lines will be adjusted to that of 59 the current indent level.""" 60 self.in_indent_lines = False 61 for l in re.split(r'\r?\n', block): 62 self.line_buffer.append(l) 63 self._update_lineno(1) 64 65 def writelines(self, *lines): 66 """print a series of lines of python.""" 67 for line in lines: 68 self.writeline(line) 69 70 def writeline(self, line): 71 """print a line of python, indenting it according to the current 72 indent level. 73 74 this also adjusts the indentation counter according to the 75 content of the line. 76 77 """ 78 79 if not self.in_indent_lines: 80 self._flush_adjusted_lines() 81 self.in_indent_lines = True 82 83 if (line is None or 84 re.match(r"^\s*#",line) or 85 re.match(r"^\s*$", line) 86 ): 87 hastext = False 88 else: 89 hastext = True 90 91 is_comment = line and len(line) and line[0] == '#' 92 93 # see if this line should decrease the indentation level 94 if (not is_comment and 95 (not hastext or self._is_unindentor(line)) 96 ): 97 98 if self.indent > 0: 99 self.indent -= 1 100 # if the indent_detail stack is empty, the user 101 # probably put extra closures - the resulting 102 # module wont compile. 103 if len(self.indent_detail) == 0: 104 raise exceptions.SyntaxException( 105 "Too many whitespace closures") 106 self.indent_detail.pop() 107 108 if line is None: 109 return 110 111 # write the line 112 self.stream.write(self._indent_line(line) + "\n") 113 self._update_lineno(len(line.split("\n"))) 114 115 # see if this line should increase the indentation level. 116 # note that a line can both decrase (before printing) and 117 # then increase (after printing) the indentation level. 118 119 if re.search(r":[ \t]*(?:#.*)?$", line): 120 # increment indentation count, and also 121 # keep track of what the keyword was that indented us, 122 # if it is a python compound statement keyword 123 # where we might have to look for an "unindent" keyword 124 match = re.match(r"^\s*(if|try|elif|while|for|with)", line) 125 if match: 126 # its a "compound" keyword, so we will check for "unindentors" 127 indentor = match.group(1) 128 self.indent += 1 129 self.indent_detail.append(indentor) 130 else: 131 indentor = None 132 # its not a "compound" keyword. but lets also 133 # test for valid Python keywords that might be indenting us, 134 # else assume its a non-indenting line 135 m2 = re.match(r"^\s*(def|class|else|elif|except|finally)", 136 line) 137 if m2: 138 self.indent += 1 139 self.indent_detail.append(indentor) 140 141 def close(self): 142 """close this printer, flushing any remaining lines.""" 143 self._flush_adjusted_lines() 144 145 def _is_unindentor(self, line): 146 """return true if the given line is an 'unindentor', 147 relative to the last 'indent' event received. 148 149 """ 150 151 # no indentation detail has been pushed on; return False 152 if len(self.indent_detail) == 0: 153 return False 154 155 indentor = self.indent_detail[-1] 156 157 # the last indent keyword we grabbed is not a 158 # compound statement keyword; return False 159 if indentor is None: 160 return False 161 162 # if the current line doesnt have one of the "unindentor" keywords, 163 # return False 164 match = re.match(r"^\s*(else|elif|except|finally).*\:", line) 165 if not match: 166 return False 167 168 # whitespace matches up, we have a compound indentor, 169 # and this line has an unindentor, this 170 # is probably good enough 171 return True 172 173 # should we decide that its not good enough, heres 174 # more stuff to check. 175 #keyword = match.group(1) 176 177 # match the original indent keyword 178 #for crit in [ 179 # (r'if|elif', r'else|elif'), 180 # (r'try', r'except|finally|else'), 181 # (r'while|for', r'else'), 182 #]: 183 # if re.match(crit[0], indentor) and re.match(crit[1], keyword): 184 # return True 185 186 #return False 187 188 def _indent_line(self, line, stripspace=''): 189 """indent the given line according to the current indent level. 190 191 stripspace is a string of space that will be truncated from the 192 start of the line before indenting.""" 193 194 return re.sub(r"^%s" % stripspace, self.indentstring 195 * self.indent, line) 196 197 def _reset_multi_line_flags(self): 198 """reset the flags which would indicate we are in a backslashed 199 or triple-quoted section.""" 200 201 self.backslashed, self.triplequoted = False, False 202 203 def _in_multi_line(self, line): 204 """return true if the given line is part of a multi-line block, 205 via backslash or triple-quote.""" 206 207 # we are only looking for explicitly joined lines here, not 208 # implicit ones (i.e. brackets, braces etc.). this is just to 209 # guard against the possibility of modifying the space inside of 210 # a literal multiline string with unfortunately placed 211 # whitespace 212 213 current_state = (self.backslashed or self.triplequoted) 214 215 if re.search(r"\\$", line): 216 self.backslashed = True 217 else: 218 self.backslashed = False 219 220 triples = len(re.findall(r"\"\"\"|\'\'\'", line)) 221 if triples == 1 or triples % 2 != 0: 222 self.triplequoted = not self.triplequoted 223 224 return current_state 225 226 def _flush_adjusted_lines(self): 227 stripspace = None 228 self._reset_multi_line_flags() 229 230 for entry in self.line_buffer: 231 if self._in_multi_line(entry): 232 self.stream.write(entry + "\n") 233 else: 234 entry = entry.expandtabs() 235 if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry): 236 stripspace = re.match(r"^([ \t]*)", entry).group(1) 237 self.stream.write(self._indent_line(entry, stripspace) + "\n") 238 239 self.line_buffer = [] 240 self._reset_multi_line_flags() 241 242 243def adjust_whitespace(text): 244 """remove the left-whitespace margin of a block of Python code.""" 245 246 state = [False, False] 247 (backslashed, triplequoted) = (0, 1) 248 249 def in_multi_line(line): 250 start_state = (state[backslashed] or state[triplequoted]) 251 252 if re.search(r"\\$", line): 253 state[backslashed] = True 254 else: 255 state[backslashed] = False 256 257 def match(reg, t): 258 m = re.match(reg, t) 259 if m: 260 return m, t[len(m.group(0)):] 261 else: 262 return None, t 263 264 while line: 265 if state[triplequoted]: 266 m, line = match(r"%s" % state[triplequoted], line) 267 if m: 268 state[triplequoted] = False 269 else: 270 m, line = match(r".*?(?=%s|$)" % state[triplequoted], line) 271 else: 272 m, line = match(r'#', line) 273 if m: 274 return start_state 275 276 m, line = match(r"\"\"\"|\'\'\'", line) 277 if m: 278 state[triplequoted] = m.group(0) 279 continue 280 281 m, line = match(r".*?(?=\"\"\"|\'\'\'|#|$)", line) 282 283 return start_state 284 285 def _indent_line(line, stripspace=''): 286 return re.sub(r"^%s" % stripspace, '', line) 287 288 lines = [] 289 stripspace = None 290 291 for line in re.split(r'\r?\n', text): 292 if in_multi_line(line): 293 lines.append(line) 294 else: 295 line = line.expandtabs() 296 if stripspace is None and re.search(r"^[ \t]*[^# \t]", line): 297 stripspace = re.match(r"^([ \t]*)", line).group(1) 298 lines.append(_indent_line(line, stripspace)) 299 return "\n".join(lines) 300