Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# mako/pygen.py 

2# Copyright 2006-2020 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(object): 

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 def _update_lineno(self, num): 

47 self.lineno += num 

48 

49 def start_source(self, lineno): 

50 if self.lineno not in self.source_map: 

51 self.source_map[self.lineno] = lineno 

52 

53 def write_blanks(self, num): 

54 self.stream.write("\n" * num) 

55 self._update_lineno(num) 

56 

57 def write_indented_block(self, block, starting_lineno=None): 

58 """print a line or lines of python which already contain indentation. 

59 

60 The indentation of the total block of lines will be adjusted to that of 

61 the current indent level.""" 

62 self.in_indent_lines = False 

63 for i, l in enumerate(re.split(r"\r?\n", block)): 

64 self.line_buffer.append(l) 

65 if starting_lineno is not None: 

66 self.start_source(starting_lineno + i) 

67 self._update_lineno(1) 

68 

69 def writelines(self, *lines): 

70 """print a series of lines of python.""" 

71 for line in lines: 

72 self.writeline(line) 

73 

74 def writeline(self, line): 

75 """print a line of python, indenting it according to the current 

76 indent level. 

77 

78 this also adjusts the indentation counter according to the 

79 content of the line. 

80 

81 """ 

82 

83 if not self.in_indent_lines: 

84 self._flush_adjusted_lines() 

85 self.in_indent_lines = True 

86 

87 if ( 

88 line is None 

89 or re.match(r"^\s*#", line) 

90 or re.match(r"^\s*$", line) 

91 ): 

92 hastext = False 

93 else: 

94 hastext = True 

95 

96 is_comment = line and len(line) and line[0] == "#" 

97 

98 # see if this line should decrease the indentation level 

99 if not is_comment and (not hastext or self._is_unindentor(line)): 

100 

101 if self.indent > 0: 

102 self.indent -= 1 

103 # if the indent_detail stack is empty, the user 

104 # probably put extra closures - the resulting 

105 # module wont compile. 

106 if len(self.indent_detail) == 0: 

107 raise exceptions.SyntaxException( 

108 "Too many whitespace closures" 

109 ) 

110 self.indent_detail.pop() 

111 

112 if line is None: 

113 return 

114 

115 # write the line 

116 self.stream.write(self._indent_line(line) + "\n") 

117 self._update_lineno(len(line.split("\n"))) 

118 

119 # see if this line should increase the indentation level. 

120 # note that a line can both decrase (before printing) and 

121 # then increase (after printing) the indentation level. 

122 

123 if re.search(r":[ \t]*(?:#.*)?$", line): 

124 # increment indentation count, and also 

125 # keep track of what the keyword was that indented us, 

126 # if it is a python compound statement keyword 

127 # where we might have to look for an "unindent" keyword 

128 match = re.match(r"^\s*(if|try|elif|while|for|with)", line) 

129 if match: 

130 # its a "compound" keyword, so we will check for "unindentors" 

131 indentor = match.group(1) 

132 self.indent += 1 

133 self.indent_detail.append(indentor) 

134 else: 

135 indentor = None 

136 # its not a "compound" keyword. but lets also 

137 # test for valid Python keywords that might be indenting us, 

138 # else assume its a non-indenting line 

139 m2 = re.match( 

140 r"^\s*(def|class|else|elif|except|finally)", line 

141 ) 

142 if m2: 

143 self.indent += 1 

144 self.indent_detail.append(indentor) 

145 

146 def close(self): 

147 """close this printer, flushing any remaining lines.""" 

148 self._flush_adjusted_lines() 

149 

150 def _is_unindentor(self, line): 

151 """return true if the given line is an 'unindentor', 

152 relative to the last 'indent' event received. 

153 

154 """ 

155 

156 # no indentation detail has been pushed on; return False 

157 if len(self.indent_detail) == 0: 

158 return False 

159 

160 indentor = self.indent_detail[-1] 

161 

162 # the last indent keyword we grabbed is not a 

163 # compound statement keyword; return False 

164 if indentor is None: 

165 return False 

166 

167 # if the current line doesnt have one of the "unindentor" keywords, 

168 # return False 

169 match = re.match(r"^\s*(else|elif|except|finally).*\:", line) 

170 if not match: 

171 return False 

172 

173 # whitespace matches up, we have a compound indentor, 

174 # and this line has an unindentor, this 

175 # is probably good enough 

176 return True 

177 

178 # should we decide that its not good enough, heres 

179 # more stuff to check. 

180 # keyword = match.group(1) 

181 

182 # match the original indent keyword 

183 # for crit in [ 

184 # (r'if|elif', r'else|elif'), 

185 # (r'try', r'except|finally|else'), 

186 # (r'while|for', r'else'), 

187 # ]: 

188 # if re.match(crit[0], indentor) and re.match(crit[1], keyword): 

189 # return True 

190 

191 # return False 

192 

193 def _indent_line(self, line, stripspace=""): 

194 """indent the given line according to the current indent level. 

195 

196 stripspace is a string of space that will be truncated from the 

197 start of the line before indenting.""" 

198 

199 return re.sub( 

200 r"^%s" % stripspace, self.indentstring * self.indent, line 

201 ) 

202 

203 def _reset_multi_line_flags(self): 

204 """reset the flags which would indicate we are in a backslashed 

205 or triple-quoted section.""" 

206 

207 self.backslashed, self.triplequoted = False, False 

208 

209 def _in_multi_line(self, line): 

210 """return true if the given line is part of a multi-line block, 

211 via backslash or triple-quote.""" 

212 

213 # we are only looking for explicitly joined lines here, not 

214 # implicit ones (i.e. brackets, braces etc.). this is just to 

215 # guard against the possibility of modifying the space inside of 

216 # a literal multiline string with unfortunately placed 

217 # whitespace 

218 

219 current_state = self.backslashed or self.triplequoted 

220 

221 if re.search(r"\\$", line): 

222 self.backslashed = True 

223 else: 

224 self.backslashed = False 

225 

226 triples = len(re.findall(r"\"\"\"|\'\'\'", line)) 

227 if triples == 1 or triples % 2 != 0: 

228 self.triplequoted = not self.triplequoted 

229 

230 return current_state 

231 

232 def _flush_adjusted_lines(self): 

233 stripspace = None 

234 self._reset_multi_line_flags() 

235 

236 for entry in self.line_buffer: 

237 if self._in_multi_line(entry): 

238 self.stream.write(entry + "\n") 

239 else: 

240 entry = entry.expandtabs() 

241 if stripspace is None and re.search(r"^[ \t]*[^# \t]", entry): 

242 stripspace = re.match(r"^([ \t]*)", entry).group(1) 

243 self.stream.write(self._indent_line(entry, stripspace) + "\n") 

244 

245 self.line_buffer = [] 

246 self._reset_multi_line_flags() 

247 

248 

249def adjust_whitespace(text): 

250 """remove the left-whitespace margin of a block of Python code.""" 

251 

252 state = [False, False] 

253 (backslashed, triplequoted) = (0, 1) 

254 

255 def in_multi_line(line): 

256 start_state = state[backslashed] or state[triplequoted] 

257 

258 if re.search(r"\\$", line): 

259 state[backslashed] = True 

260 else: 

261 state[backslashed] = False 

262 

263 def match(reg, t): 

264 m = re.match(reg, t) 

265 if m: 

266 return m, t[len(m.group(0)) :] 

267 else: 

268 return None, t 

269 

270 while line: 

271 if state[triplequoted]: 

272 m, line = match(r"%s" % state[triplequoted], line) 

273 if m: 

274 state[triplequoted] = False 

275 else: 

276 m, line = match(r".*?(?=%s|$)" % state[triplequoted], line) 

277 else: 

278 m, line = match(r"#", line) 

279 if m: 

280 return start_state 

281 

282 m, line = match(r"\"\"\"|\'\'\'", line) 

283 if m: 

284 state[triplequoted] = m.group(0) 

285 continue 

286 

287 m, line = match(r".*?(?=\"\"\"|\'\'\'|#|$)", line) 

288 

289 return start_state 

290 

291 def _indent_line(line, stripspace=""): 

292 return re.sub(r"^%s" % stripspace, "", line) 

293 

294 lines = [] 

295 stripspace = None 

296 

297 for line in re.split(r"\r?\n", text): 

298 if in_multi_line(line): 

299 lines.append(line) 

300 else: 

301 line = line.expandtabs() 

302 if stripspace is None and re.search(r"^[ \t]*[^# \t]", line): 

303 stripspace = re.match(r"^([ \t]*)", line).group(1) 

304 lines.append(_indent_line(line, stripspace)) 

305 return "\n".join(lines)