1# mako/exceptions.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"""exception classes"""
8
9import traceback
10import sys
11from mako import util, compat
12
13class MakoException(Exception):
14    pass
15
16class RuntimeException(MakoException):
17    pass
18
19def _format_filepos(lineno, pos, filename):
20    if filename is None:
21        return " at line: %d char: %d" % (lineno, pos)
22    else:
23        return " in file '%s' at line: %d char: %d" % (filename, lineno, pos)
24
25
26class CompileException(MakoException):
27    def __init__(self, message, source, lineno, pos, filename):
28        MakoException.__init__(self,
29                              message + _format_filepos(lineno, pos, filename))
30        self.lineno = lineno
31        self.pos = pos
32        self.filename = filename
33        self.source = source
34
35class SyntaxException(MakoException):
36    def __init__(self, message, source, lineno, pos, filename):
37        MakoException.__init__(self,
38                              message + _format_filepos(lineno, pos, filename))
39        self.lineno = lineno
40        self.pos = pos
41        self.filename = filename
42        self.source = source
43
44class UnsupportedError(MakoException):
45    """raised when a retired feature is used."""
46
47class NameConflictError(MakoException):
48    """raised when a reserved word is used inappropriately"""
49
50class TemplateLookupException(MakoException):
51    pass
52
53class TopLevelLookupException(TemplateLookupException):
54    pass
55
56class RichTraceback(object):
57    """Pull the current exception from the ``sys`` traceback and extracts
58    Mako-specific template information.
59
60    See the usage examples in :ref:`handling_exceptions`.
61
62    """
63    def __init__(self, error=None, traceback=None):
64        self.source, self.lineno = "", 0
65
66        if error is None or traceback is None:
67            t, value, tback = sys.exc_info()
68
69        if error is None:
70            error = value or t
71
72        if traceback is None:
73            traceback = tback
74
75        self.error = error
76        self.records = self._init(traceback)
77
78        if isinstance(self.error, (CompileException, SyntaxException)):
79            self.source = self.error.source
80            self.lineno = self.error.lineno
81            self._has_source = True
82
83        self._init_message()
84
85    @property
86    def errorname(self):
87        return compat.exception_name(self.error)
88
89    def _init_message(self):
90        """Find a unicode representation of self.error"""
91        try:
92            self.message = compat.text_type(self.error)
93        except UnicodeError:
94            try:
95                self.message = str(self.error)
96            except UnicodeEncodeError:
97                # Fallback to args as neither unicode nor
98                # str(Exception(u'\xe6')) work in Python < 2.6
99                self.message = self.error.args[0]
100        if not isinstance(self.message, compat.text_type):
101            self.message = compat.text_type(self.message, 'ascii', 'replace')
102
103    def _get_reformatted_records(self, records):
104        for rec in records:
105            if rec[6] is not None:
106                yield (rec[4], rec[5], rec[2], rec[6])
107            else:
108                yield tuple(rec[0:4])
109
110    @property
111    def traceback(self):
112        """Return a list of 4-tuple traceback records (i.e. normal python
113        format) with template-corresponding lines remapped to the originating
114        template.
115
116        """
117        return list(self._get_reformatted_records(self.records))
118
119    @property
120    def reverse_records(self):
121        return reversed(self.records)
122
123    @property
124    def reverse_traceback(self):
125        """Return the same data as traceback, except in reverse order.
126        """
127
128        return list(self._get_reformatted_records(self.reverse_records))
129
130    def _init(self, trcback):
131        """format a traceback from sys.exc_info() into 7-item tuples,
132        containing the regular four traceback tuple items, plus the original
133        template filename, the line number adjusted relative to the template
134        source, and code line from that line number of the template."""
135
136        import mako.template
137        mods = {}
138        rawrecords = traceback.extract_tb(trcback)
139        new_trcback = []
140        for filename, lineno, function, line in rawrecords:
141            if not line:
142                line = ''
143            try:
144                (line_map, template_lines) = mods[filename]
145            except KeyError:
146                try:
147                    info = mako.template._get_module_info(filename)
148                    module_source = info.code
149                    template_source = info.source
150                    template_filename = info.template_filename or filename
151                except KeyError:
152                    # A normal .py file (not a Template)
153                    if not compat.py3k:
154                        try:
155                            fp = open(filename, 'rb')
156                            encoding = util.parse_encoding(fp)
157                            fp.close()
158                        except IOError:
159                            encoding = None
160                        if encoding:
161                            line = line.decode(encoding)
162                        else:
163                            line = line.decode('ascii', 'replace')
164                    new_trcback.append((filename, lineno, function, line,
165                                            None, None, None, None))
166                    continue
167
168                template_ln = 1
169
170                source_map = mako.template.ModuleInfo.\
171                                get_module_source_metadata(
172                                    module_source, full_line_map=True)
173                line_map = source_map['full_line_map']
174
175                template_lines = [line for line in
176                                    template_source.split("\n")]
177                mods[filename] = (line_map, template_lines)
178
179            template_ln = line_map[lineno - 1]
180
181            if template_ln <= len(template_lines):
182                template_line = template_lines[template_ln - 1]
183            else:
184                template_line = None
185            new_trcback.append((filename, lineno, function,
186                                line, template_filename, template_ln,
187                                template_line, template_source))
188        if not self.source:
189            for l in range(len(new_trcback) - 1, 0, -1):
190                if new_trcback[l][5]:
191                    self.source = new_trcback[l][7]
192                    self.lineno = new_trcback[l][5]
193                    break
194            else:
195                if new_trcback:
196                    try:
197                        # A normal .py file (not a Template)
198                        fp = open(new_trcback[-1][0], 'rb')
199                        encoding = util.parse_encoding(fp)
200                        fp.seek(0)
201                        self.source = fp.read()
202                        fp.close()
203                        if encoding:
204                            self.source = self.source.decode(encoding)
205                    except IOError:
206                        self.source = ''
207                    self.lineno = new_trcback[-1][1]
208        return new_trcback
209
210
211def text_error_template(lookup=None):
212    """Provides a template that renders a stack trace in a similar format to
213    the Python interpreter, substituting source template filenames, line
214    numbers and code for that of the originating source template, as
215    applicable.
216
217    """
218    import mako.template
219    return mako.template.Template(r"""
220<%page args="error=None, traceback=None"/>
221<%!
222    from mako.exceptions import RichTraceback
223%>\
224<%
225    tback = RichTraceback(error=error, traceback=traceback)
226%>\
227Traceback (most recent call last):
228% for (filename, lineno, function, line) in tback.traceback:
229  File "${filename}", line ${lineno}, in ${function or '?'}
230    ${line | trim}
231% endfor
232${tback.errorname}: ${tback.message}
233""")
234
235
236def _install_pygments():
237    global syntax_highlight, pygments_html_formatter
238    from mako.ext.pygmentplugin import syntax_highlight,\
239            pygments_html_formatter
240
241def _install_fallback():
242    global syntax_highlight, pygments_html_formatter
243    from mako.filters import html_escape
244    pygments_html_formatter = None
245    def syntax_highlight(filename='', language=None):
246        return html_escape
247
248def _install_highlighting():
249    try:
250        _install_pygments()
251    except ImportError:
252        _install_fallback()
253_install_highlighting()
254
255def html_error_template():
256    """Provides a template that renders a stack trace in an HTML format,
257    providing an excerpt of code as well as substituting source template
258    filenames, line numbers and code for that of the originating source
259    template, as applicable.
260
261    The template's default ``encoding_errors`` value is
262    ``'htmlentityreplace'``. The template has two options. With the
263    ``full`` option disabled, only a section of an HTML document is
264    returned. With the ``css`` option disabled, the default stylesheet
265    won't be included.
266
267    """
268    import mako.template
269    return mako.template.Template(r"""
270<%!
271    from mako.exceptions import RichTraceback, syntax_highlight,\
272            pygments_html_formatter
273%>
274<%page args="full=True, css=True, error=None, traceback=None"/>
275% if full:
276<html>
277<head>
278    <title>Mako Runtime Error</title>
279% endif
280% if css:
281    <style>
282        body { font-family:verdana; margin:10px 30px 10px 30px;}
283        .stacktrace { margin:5px 5px 5px 5px; }
284        .highlight { padding:0px 10px 0px 10px; background-color:#9F9FDF; }
285        .nonhighlight { padding:0px; background-color:#DFDFDF; }
286        .sample { padding:10px; margin:10px 10px 10px 10px;
287                  font-family:monospace; }
288        .sampleline { padding:0px 10px 0px 10px; }
289        .sourceline { margin:5px 5px 10px 5px; font-family:monospace;}
290        .location { font-size:80%; }
291        .highlight { white-space:pre; }
292        .sampleline { white-space:pre; }
293
294    % if pygments_html_formatter:
295        ${pygments_html_formatter.get_style_defs()}
296        .linenos { min-width: 2.5em; text-align: right; }
297        pre { margin: 0; }
298        .syntax-highlighted { padding: 0 10px; }
299        .syntax-highlightedtable { border-spacing: 1px; }
300        .nonhighlight { border-top: 1px solid #DFDFDF;
301                        border-bottom: 1px solid #DFDFDF; }
302        .stacktrace .nonhighlight { margin: 5px 15px 10px; }
303        .sourceline { margin: 0 0; font-family:monospace; }
304        .code { background-color: #F8F8F8; width: 100%; }
305        .error .code { background-color: #FFBDBD; }
306        .error .syntax-highlighted { background-color: #FFBDBD; }
307    % endif
308
309    </style>
310% endif
311% if full:
312</head>
313<body>
314% endif
315
316<h2>Error !</h2>
317<%
318    tback = RichTraceback(error=error, traceback=traceback)
319    src = tback.source
320    line = tback.lineno
321    if src:
322        lines = src.split('\n')
323    else:
324        lines = None
325%>
326<h3>${tback.errorname}: ${tback.message|h}</h3>
327
328% if lines:
329    <div class="sample">
330    <div class="nonhighlight">
331% for index in range(max(0, line-4),min(len(lines), line+5)):
332    <%
333       if pygments_html_formatter:
334           pygments_html_formatter.linenostart = index + 1
335    %>
336    % if index + 1 == line:
337    <%
338       if pygments_html_formatter:
339           old_cssclass = pygments_html_formatter.cssclass
340           pygments_html_formatter.cssclass = 'error ' + old_cssclass
341    %>
342        ${lines[index] | syntax_highlight(language='mako')}
343    <%
344       if pygments_html_formatter:
345           pygments_html_formatter.cssclass = old_cssclass
346    %>
347    % else:
348        ${lines[index] | syntax_highlight(language='mako')}
349    % endif
350% endfor
351    </div>
352    </div>
353% endif
354
355<div class="stacktrace">
356% for (filename, lineno, function, line) in tback.reverse_traceback:
357    <div class="location">${filename}, line ${lineno}:</div>
358    <div class="nonhighlight">
359    <%
360       if pygments_html_formatter:
361           pygments_html_formatter.linenostart = lineno
362    %>
363      <div class="sourceline">${line | syntax_highlight(filename)}</div>
364    </div>
365% endfor
366</div>
367
368% if full:
369</body>
370</html>
371% endif
372""", output_encoding=sys.getdefaultencoding(),
373        encoding_errors='htmlentityreplace')
374