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