diff --git a/CHANGES b/CHANGES index 41db8fc..c2b1b09 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +0.2.3 +- fixed the html_error_template not handling tracebacks from +normal .py files with a magic encoding comment [ticket:88] + 0.2.2 - cached blocks now use the current context when rendering an expired section, instead of the original context diff --git a/lib/mako/exceptions.py b/lib/mako/exceptions.py index e665a7f..3f4e3a7 100644 --- a/lib/mako/exceptions.py +++ b/lib/mako/exceptions.py @@ -7,6 +7,7 @@ """exception classes""" import traceback, sys, re +from mako import util class MakoException(Exception): pass @@ -139,7 +140,14 @@ def _init(self): break else: try: - self.source = file(new_trcback[-1][0]).read() + # A normal .py file (not a Template) + fp = open(new_trcback[-1][0]) + encoding = util.parse_encoding(fp) + fp.seek(0) + self.source = fp.read() + fp.close() + if encoding: + self.source = self.source.decode(encoding) except IOError: self.source = '' self.lineno = new_trcback[-1][1] diff --git a/lib/mako/util.py b/lib/mako/util.py index e582792..58a18a7 100644 --- a/lib/mako/util.py +++ b/lib/mako/util.py @@ -16,7 +16,7 @@ except: from StringIO import StringIO -import weakref, os, time +import codecs, re, weakref, os, time try: import threading @@ -130,6 +130,56 @@ def _manage_size(self): # on us. loop around and try again break +# Regexp to match python magic encoding line +_PYTHON_MAGIC_COMMENT_re = re.compile( + r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)', + re.VERBOSE) + +def parse_encoding(fp): + """Deduce the encoding of a source file from magic comment. + + It does this in the same way as the `Python interpreter`__ + + .. __: http://docs.python.org/ref/encodings.html + + The ``fp`` argument should be a seekable file object. + """ + pos = fp.tell() + fp.seek(0) + try: + line1 = fp.readline() + has_bom = line1.startswith(codecs.BOM_UTF8) + if has_bom: + line1 = line1[len(codecs.BOM_UTF8):] + + m = _PYTHON_MAGIC_COMMENT_re.match(line1) + if not m: + try: + import parser + parser.suite(line1) + except (ImportError, SyntaxError): + # Either it's a real syntax error, in which case the source + # is not valid python source, or line2 is a continuation of + # line1, in which case we don't want to scan line2 for a magic + # comment. + pass + else: + line2 = fp.readline() + m = _PYTHON_MAGIC_COMMENT_re.match(line2) + + if has_bom: + if m: + raise SyntaxError, \ + "python refuses to compile code with both a UTF8" \ + " byte-order-mark and a magic encoding comment" + return 'utf_8' + elif m: + return m.group(1) + else: + return None + finally: + fp.seek(pos) + def restore__ast(_ast): """Attempt to restore the required classes to the _ast module if it appears to be missing them diff --git a/test/exceptions_.py b/test/exceptions_.py index a5f1657..43059f5 100644 --- a/test/exceptions_.py +++ b/test/exceptions_.py @@ -60,6 +60,15 @@ def test_utf8_html_error_template(self): assert False, ("This function should trigger a CompileException, " "but didn't") + def test_py_utf8_html_error_template(self): + try: + foo = u'日本' + raise RuntimeError('test') + except: + html_error = exceptions.html_error_template().render() + assert 'RuntimeError: test' in html_error + assert "foo = u'日本'" in html_error + def test_format_exceptions(self): l = TemplateLookup(format_exceptions=True)