From a18872d1b8a10a82aa96d41bce6f112c1187cff0 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 5 Mar 2009 23:47:00 +0100 Subject: [PATCH] Started refactoring of debugging system for better AppEngine/Pylons support. --HG-- branch : trunk extra : rebase_source : 30b87a402e0847f95eaf277d0fc50e1a63177d5b --- jinja2/debug.py | 116 +++++++++++++++++++++++++++++++++++------- jinja2/environment.py | 40 ++++++++++----- 2 files changed, 126 insertions(+), 30 deletions(-) diff --git a/jinja2/debug.py b/jinja2/debug.py index d2c5a11..ce794e2 100644 --- a/jinja2/debug.py +++ b/jinja2/debug.py @@ -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,) diff --git a/jinja2/environment.py b/jinja2/environment.py index 23bf24b..c2fee16 100644 --- a/jinja2/environment.py +++ b/jinja2/environment.py @@ -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 -- 2.26.2