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