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