Started refactoring of debugging system for better AppEngine/Pylons support.
authorArmin Ronacher <armin.ronacher@active-4.com>
Thu, 5 Mar 2009 22:47:00 +0000 (23:47 +0100)
committerArmin Ronacher <armin.ronacher@active-4.com>
Thu, 5 Mar 2009 22:47:00 +0000 (23:47 +0100)
--HG--
branch : trunk
extra : rebase_source : 30b87a402e0847f95eaf277d0fc50e1a63177d5b

jinja2/debug.py
jinja2/environment.py

index d2c5a116468236eb6b4e8cb310a593f627a4e144..ce794e243b38e8ae558957c9e235eae2a7f48ade 100644 (file)
@@ -8,10 +8,85 @@
     with correct line numbers, locals and contents.
 
     :copyright: (c) 2009 by the Jinja Team.
-    :license: BSD.
+    :license: BSD, see LICENSE for more details.
 """
 import sys
+import traceback
 from jinja2.utils import CodeType, missing, internal_code
+from jinja2.exceptions import TemplateSyntaxError
+
+
+class TracebackFrameProxy(object):
+    """Proxies a traceback frame."""
+
+    def __init__(self, tb):
+        self.tb = tb
+
+    def _set_tb_next(self, next):
+        if tb_set_next is not None:
+            tb_set_next(self.tb, next and next.tb or None)
+        self._tb_next = next
+
+    def _get_tb_next(self):
+        return self._tb_next
+
+    tb_next = property(_get_tb_next, _set_tb_next)
+    del _get_tb_next, _set_tb_next
+
+    @property
+    def is_jinja_frame(self):
+        return '__jinja_template__' in self.tb.tb_frame.f_globals
+
+    def __getattr__(self, name):
+        return getattr(self.tb, name)
+
+
+class ProcessedTraceback(object):
+    """Holds a Jinja preprocessed traceback for priting or reraising."""
+
+    def __init__(self, exc_type, exc_value, frames):
+        assert frames, 'no frames for this traceback?'
+        self.exc_type = exc_type
+        self.exc_value = exc_value
+        self.frames = frames
+
+    def chain_frames(self):
+        """Chains the frames.  Requires ctypes or the speedups extension."""
+        prev_tb = None
+        for tb in self.frames:
+            if prev_tb is not None:
+                prev_tb.tb_next = tb
+            prev_tb = tb
+        prev_tb.tb_next = None
+
+    def render_as_text(self, limit=None):
+        """Return a string with the traceback."""
+        lines = traceback.format_exception(self.exc_type, self.exc_value,
+                                           self.frames[0], limit=limit)
+        return ''.join(lines).rstrip()
+
+    @property
+    def is_template_syntax_error(self):
+        """`True` if this is a template syntax error."""
+        return isinstance(self.exc_value, TemplateSyntaxError)
+
+    @property
+    def exc_info(self):
+        """Exception info tuple with a proxy around the frame objects."""
+        return self.exc_type, self.exc_value, self.frames[0]
+
+    @property
+    def standard_exc_info(self):
+        """Standard python exc_info for re-raising"""
+        return self.exc_type, self.exc_value, self.frames[0].tb
+
+
+def make_traceback(exc_info, source_hint=None):
+    """Creates a processed traceback object from the exc_info."""
+    exc_type, exc_value, tb = exc_info
+    if isinstance(exc_value, TemplateSyntaxError):
+        exc_info = translate_syntax_error(exc_value, source_hint)
+    return translate_exception(exc_info)
 
 
 def translate_syntax_error(error, source=None):
@@ -29,32 +104,44 @@ def translate_exception(exc_info):
     """If passed an exc_info it will automatically rewrite the exceptions
     all the way down to the correct line numbers and frames.
     """
-    result_tb = prev_tb = None
     initial_tb = tb = exc_info[2].tb_next
+    frames = []
 
     while tb is not None:
         # skip frames decorated with @internalcode.  These are internal
         # calls we can't avoid and that are useless in template debugging
         # output.
-        if tb_set_next is not None and tb.tb_frame.f_code in internal_code:
-            tb_set_next(prev_tb, tb.tb_next)
+        if tb.tb_frame.f_code in internal_code:
             tb = tb.tb_next
             continue
 
+        # save a reference to the next frame if we override the current
+        # one with a faked one.
+        next = tb.tb_next
+
+        # fake template exceptions
         template = tb.tb_frame.f_globals.get('__jinja_template__')
         if template is not None:
             lineno = template.get_corresponding_lineno(tb.tb_lineno)
             tb = fake_exc_info(exc_info[:2] + (tb,), template.filename,
-                               lineno, prev_tb)[2]
-        if result_tb is None:
-            result_tb = tb
-        prev_tb = tb
-        tb = tb.tb_next
+                               lineno)[2]
+
+        frames.append(TracebackFrameProxy(tb))
+        tb = next
+
+    # if we don't have any exceptions in the frames left, we have to
+    # reraise it unchanged.
+    # XXX: can we backup here?  when could this happen?
+    if not frames:
+        raise exc_info[0], exc_info[1], exc_info[2]
 
-    return exc_info[:2] + (result_tb or initial_tb,)
+    traceback = ProcessedTraceback(exc_info[0], exc_info[1], frames)
+    if tb_set_next is not None:
+        traceback.chain_frames()
+    return traceback
 
 
-def fake_exc_info(exc_info, filename, lineno, tb_back=None):
+def fake_exc_info(exc_info, filename, lineno):
     """Helper for `translate_exception`."""
     exc_type, exc_value, tb = exc_info
 
@@ -115,13 +202,6 @@ def fake_exc_info(exc_info, filename, lineno, tb_back=None):
         exc_info = sys.exc_info()
         new_tb = exc_info[2].tb_next
 
-    # now we can patch the exc info accordingly
-    if tb_set_next is not None:
-        if tb_back is not None:
-            tb_set_next(tb_back, new_tb)
-        if tb is not None:
-            tb_set_next(new_tb, tb.tb_next)
-
     # return without this frame
     return exc_info[:2] + (new_tb,)
 
index 23bf24b8987adeef09299b807fbb720bc2a8ba56..c2fee169844982ac8bd5b662c049935fb1940f7c 100644 (file)
@@ -24,6 +24,10 @@ from jinja2.utils import import_string, LRUCache, Markup, missing, \
 # for direct template usage we have up to ten living environments
 _spontaneous_environments = LRUCache(10)
 
+# the function to create jinja traceback objects.  This is dynamically
+# imported on the first exception in the exception handler.
+_make_traceback = None
+
 
 def get_spontaneous_environment(*args):
     """Return a new spontaneous environment.  A spontaneous environment is an
@@ -196,6 +200,9 @@ class Environment(object):
     #: must not be modified
     shared = False
 
+    exception_handler = None
+    exception_formatter = None
+
     def __init__(self,
                  block_start_string=BLOCK_START_STRING,
                  block_end_string=BLOCK_END_STRING,
@@ -362,9 +369,7 @@ class Environment(object):
         try:
             return Parser(self, source, name, filename).parse()
         except TemplateSyntaxError, e:
-            from jinja2.debug import translate_syntax_error
-            exc_type, exc_value, tb = translate_syntax_error(e, source)
-            raise exc_type, exc_value, tb
+            self.handle_exception(sys.exc_info(), source_hint=source)
 
     def lex(self, source, name=None, filename=None):
         """Lex the given sourcecode and return a generator that yields
@@ -380,9 +385,7 @@ class Environment(object):
         try:
             return self.lexer.tokeniter(source, name, filename)
         except TemplateSyntaxError, e:
-            from jinja2.debug import translate_syntax_error
-            exc_type, exc_value, tb = translate_syntax_error(e, source)
-            raise exc_type, exc_value, tb
+            self.handle_exception(sys.exc_info(), source_hint=source)
 
     def preprocess(self, source, name=None, filename=None):
         """Preprocesses the source with all extensions.  This is automatically
@@ -473,6 +476,23 @@ class Environment(object):
         template = self.from_string(nodes.Template(body, lineno=1))
         return TemplateExpression(template, undefined_to_none)
 
+    def handle_exception(self, exc_info=None, rendered=False, source_hint=None):
+        """Exception handling helper.  This is used internally to either raise
+        rewritten exceptions or return a rendered traceback for the template.
+        """
+        global _make_traceback
+        if exc_info is None:
+            exc_info = sys.exc_info()
+        if _make_traceback is None:
+            from jinja2.debug import make_traceback as _make_traceback
+        traceback = _make_traceback(exc_info, source_hint)
+        if rendered and self.exception_formatter is not None:
+            return self.exception_formatter(traceback)
+        if self.exception_handler is not None:
+            self.exception_handler(traceback)
+        exc_type, exc_value, tb = traceback.standard_exc_info
+        raise exc_type, exc_value, tb
+
     def join_path(self, template, parent):
         """Join a template with the parent.  By default all the lookups are
         relative to the loader root so this method returns the `template`
@@ -625,9 +645,7 @@ class Template(object):
         try:
             return concat(self.root_render_func(self.new_context(vars)))
         except:
-            from jinja2.debug import translate_exception
-            exc_type, exc_value, tb = translate_exception(sys.exc_info())
-            raise exc_type, exc_value, tb
+            return self.environment.handle_exception(sys.exc_info(), True)
 
     def stream(self, *args, **kwargs):
         """Works exactly like :meth:`generate` but returns a
@@ -648,9 +666,7 @@ class Template(object):
             for event in self.root_render_func(self.new_context(vars)):
                 yield event
         except:
-            from jinja2.debug import translate_exception
-            exc_type, exc_value, tb = translate_exception(sys.exc_info())
-            raise exc_type, exc_value, tb
+            yield self.environment.handle_exception(sys.exc_info(), True)
 
     def new_context(self, vars=None, shared=False, locals=None):
         """Create a new :class:`Context` for this template.  The vars