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 
9 import sys
10 import traceback
11 
12 from mako import compat
13 from mako import util
14 
15 
16 class MakoException(Exception):
17     pass
18 
19 
20 class RuntimeException(MakoException):
21     pass
22 
23 
24 def _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 
31 class 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 
42 class 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 
53 class UnsupportedError(MakoException):
54 
55     """raised when a retired feature is used."""
56 
57 
58 class NameConflictError(MakoException):
59 
60     """raised when a reserved word is used inappropriately"""
61 
62 
63 class TemplateLookupException(MakoException):
64     pass
65 
66 
67 class TopLevelLookupException(TemplateLookupException):
68     pass
69 
70 
71 class 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 
241 def 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 %>\
259 Traceback (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 
269 def _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 
275 def _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 
285 def _install_highlighting():
286     try:
287         _install_pygments()
288     except ImportError:
289         _install_fallback()
290 
291 
292 _install_highlighting()
293 
294 
295 def 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