Improved Jinja's debugging support by introducing "@internalcode" which marks code objects that are skipped on tracebacks.  Also template errors are now translated as well to help the pylons debugger.

--HG--
branch : trunk
diff --git a/examples/basic/debugger.py b/examples/basic/debugger.py
index 52a8be2..4291ff7 100644
--- a/examples/basic/debugger.py
+++ b/examples/basic/debugger.py
@@ -4,4 +4,4 @@
 env = Environment(loader=FileSystemLoader('templates'))
 
 tmpl = env.get_template('broken.html')
-print tmpl.render(seq=range(10))
+print tmpl.render(seq=[3, 2, 4, 5, 3, 2, 0, 2, 1])
diff --git a/examples/basic/templates/broken.html b/examples/basic/templates/broken.html
index bbd5bf4..294d5c9 100644
--- a/examples/basic/templates/broken.html
+++ b/examples/basic/templates/broken.html
@@ -1,5 +1,6 @@
+{% from 'subbroken.html' import may_break %}
 <ul>
 {% for item in seq %}
-  <li>{{ item / 0 }}</li>
+  <li>{{ may_break(item) }}</li>
 {% endfor %}
 </ul>
diff --git a/examples/basic/templates/subbroken.html b/examples/basic/templates/subbroken.html
new file mode 100644
index 0000000..245eb7e
--- /dev/null
+++ b/examples/basic/templates/subbroken.html
@@ -0,0 +1,3 @@
+{% macro may_break(item) -%}
+  [{{ item / 0 }}]
+{%- endmacro %}
diff --git a/jinja2/debug.py b/jinja2/debug.py
index bfd00f1..d2c5a11 100644
--- a/jinja2/debug.py
+++ b/jinja2/debug.py
@@ -11,7 +11,18 @@
     :license: BSD.
 """
 import sys
-from jinja2.utils import CodeType, missing
+from jinja2.utils import CodeType, missing, internal_code
+
+
+def translate_syntax_error(error, source=None):
+    """Rewrites a syntax error to please traceback systems."""
+    error.source = source
+    error.translated = True
+    exc_info = (type(error), error, None)
+    filename = error.filename
+    if filename is None:
+        filename = '<unknown>'
+    return fake_exc_info(exc_info, filename, error.lineno)
 
 
 def translate_exception(exc_info):
@@ -22,6 +33,14 @@
     initial_tb = tb = exc_info[2].tb_next
 
     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)
+            tb = tb.tb_next
+            continue
+
         template = tb.tb_frame.f_globals.get('__jinja_template__')
         if template is not None:
             lineno = template.get_corresponding_lineno(tb.tb_lineno)
@@ -40,19 +59,22 @@
     exc_type, exc_value, tb = exc_info
 
     # figure the real context out
-    real_locals = tb.tb_frame.f_locals.copy()
-    ctx = real_locals.get('context')
-    if ctx:
-        locals = ctx.get_all()
+    if tb is not None:
+        real_locals = tb.tb_frame.f_locals.copy()
+        ctx = real_locals.get('context')
+        if ctx:
+            locals = ctx.get_all()
+        else:
+            locals = {}
+        for name, value in real_locals.iteritems():
+            if name.startswith('l_') and value is not missing:
+                locals[name[2:]] = value
+
+        # if there is a local called __jinja_exception__, we get
+        # rid of it to not break the debug functionality.
+        locals.pop('__jinja_exception__', None)
     else:
         locals = {}
-    for name, value in real_locals.iteritems():
-        if name.startswith('l_') and value is not missing:
-            locals[name[2:]] = value
-
-    # if there is a local called __jinja_exception__, we get
-    # rid of it to not break the debug functionality.
-    locals.pop('__jinja_exception__', None)
 
     # assamble fake globals we need
     globals = {
@@ -68,13 +90,16 @@
     # if it's possible, change the name of the code.  This won't work
     # on some python environments such as google appengine
     try:
-        function = tb.tb_frame.f_code.co_name
-        if function == 'root':
-            location = 'top-level template code'
-        elif function.startswith('block_'):
-            location = 'block "%s"' % function[6:]
-        else:
+        if tb is None:
             location = 'template'
+        else:
+            function = tb.tb_frame.f_code.co_name
+            if function == 'root':
+                location = 'top-level template code'
+            elif function.startswith('block_'):
+                location = 'block "%s"' % function[6:]
+            else:
+                location = 'template'
         code = CodeType(0, code.co_nlocals, code.co_stacksize,
                         code.co_flags, code.co_code, code.co_consts,
                         code.co_names, code.co_varnames, filename,
diff --git a/jinja2/environment.py b/jinja2/environment.py
index fcc11d2..c7f311e 100644
--- a/jinja2/environment.py
+++ b/jinja2/environment.py
@@ -18,7 +18,7 @@
 from jinja2.runtime import Undefined, new_context
 from jinja2.exceptions import TemplateSyntaxError
 from jinja2.utils import import_string, LRUCache, Markup, missing, \
-     concat, consume
+     concat, consume, internalcode
 
 
 # for direct template usage we have up to ten living environments
@@ -339,6 +339,7 @@
         except (TypeError, LookupError, AttributeError):
             return self.undefined(obj=obj, name=attribute)
 
+    @internalcode
     def parse(self, source, name=None, filename=None):
         """Parse the sourcecode and return the abstract syntax tree.  This
         tree of nodes is used by the compiler to convert the template into
@@ -353,8 +354,9 @@
         try:
             return Parser(self, source, name, filename).parse()
         except TemplateSyntaxError, e:
-            e.source = source
-            raise e
+            from jinja2.debug import translate_syntax_error
+            exc_type, exc_value, tb = translate_syntax_error(e, source)
+            raise exc_type, exc_value, tb
 
     def lex(self, source, name=None, filename=None):
         """Lex the given sourcecode and return a generator that yields
@@ -370,8 +372,9 @@
         try:
             return self.lexer.tokeniter(source, name, filename)
         except TemplateSyntaxError, e:
-            e.source = source
-            raise e
+            from jinja2.debug import translate_syntax_error
+            exc_type, exc_value, tb = translate_syntax_error(e, source)
+            raise exc_type, exc_value, tb
 
     def preprocess(self, source, name=None, filename=None):
         """Preprocesses the source with all extensions.  This is automatically
@@ -393,6 +396,7 @@
                 stream = TokenStream(stream, name, filename)
         return stream
 
+    @internalcode
     def compile(self, source, name=None, filename=None, raw=False):
         """Compile a node or template source code.  The `name` parameter is
         the load name of the template after it was joined using
@@ -473,6 +477,7 @@
         """
         return template
 
+    @internalcode
     def get_template(self, name, parent=None, globals=None):
         """Load a template from the loader.  If a loader is configured this
         method ask the loader for the template and returns a :class:`Template`.
diff --git a/jinja2/exceptions.py b/jinja2/exceptions.py
index 8311cf3..182c061 100644
--- a/jinja2/exceptions.py
+++ b/jinja2/exceptions.py
@@ -44,7 +44,16 @@
         self.filename = filename
         self.source = None
 
+        # this is set to True if the debug.translate_syntax_error
+        # function translated the syntax error into a new traceback
+        self.translated = False
+
     def __unicode__(self):
+        # for translated errors we only return the message
+        if self.translated:
+            return self.message.encode('utf-8')
+
+        # otherwise attach some stuff
         location = 'line %d' % self.lineno
         name = self.filename or self.name
         if name:
diff --git a/jinja2/loaders.py b/jinja2/loaders.py
index b5817c7..feb4ff7 100644
--- a/jinja2/loaders.py
+++ b/jinja2/loaders.py
@@ -14,7 +14,7 @@
 except ImportError:
     from sha import new as sha1
 from jinja2.exceptions import TemplateNotFound
-from jinja2.utils import LRUCache, open_if_exists
+from jinja2.utils import LRUCache, open_if_exists, internalcode
 
 
 def split_template_path(template):
@@ -79,6 +79,7 @@
         """
         raise TemplateNotFound(template)
 
+    @internalcode
     def load(self, environment, name, globals=None):
         """Loads a template.  This method looks up the template in the cache
         or loads one by calling :meth:`get_source`.  Subclasses should not
diff --git a/jinja2/runtime.py b/jinja2/runtime.py
index c2e0aa3..669ff21 100644
--- a/jinja2/runtime.py
+++ b/jinja2/runtime.py
@@ -11,7 +11,7 @@
 import sys
 from itertools import chain, imap
 from jinja2.utils import Markup, partial, soft_unicode, escape, missing, \
-     concat, MethodType, FunctionType
+     concat, MethodType, FunctionType, internalcode
 from jinja2.exceptions import UndefinedError, TemplateRuntimeError, \
      TemplateNotFound
 
@@ -156,6 +156,7 @@
         """
         return dict(self.parent, **self.vars)
 
+    @internalcode
     def call(__self, __obj, *args, **kwargs):
         """Call the callable with the arguments and keyword arguments
         provided but inject the active context or environment as first
@@ -239,6 +240,7 @@
         return BlockReference(self.name, self._context, self._stack,
                               self._depth + 1)
 
+    @internalcode
     def __call__(self):
         rv = concat(self._stack[self._depth](self._context))
         if self._context.environment.autoescape:
@@ -281,6 +283,7 @@
     def __iter__(self):
         return LoopContextIterator(self)
 
+    @internalcode
     def loop(self, iterable):
         if self._recurse is None:
             raise TypeError('Tried to call non recursive loop.  Maybe you '
@@ -342,6 +345,7 @@
         self.catch_varargs = catch_varargs
         self.caller = caller
 
+    @internalcode
     def __call__(self, *args, **kwargs):
         arguments = []
         for idx, name in enumerate(self.arguments):
@@ -409,6 +413,7 @@
         self._undefined_name = name
         self._undefined_exception = exc
 
+    @internalcode
     def _fail_with_undefined_error(self, *args, **kwargs):
         """Regular callback function for undefined objects that raises an
         `UndefinedError` on call.
diff --git a/jinja2/utils.py b/jinja2/utils.py
index 1ae38e0..6c3805a 100644
--- a/jinja2/utils.py
+++ b/jinja2/utils.py
@@ -35,6 +35,9 @@
 # special singleton representing missing values for the runtime
 missing = type('MissingType', (), {'__repr__': lambda x: 'missing'})()
 
+# internal code
+internal_code = set()
+
 
 # concatenate a list of strings and convert them to unicode.
 # unfortunately there is a bug in python 2.4 and lower that causes
@@ -120,6 +123,12 @@
     return f
 
 
+def internalcode(f):
+    """Marks the function as internally used"""
+    internal_code.add(f.func_code)
+    return f
+
+
 def is_undefined(obj):
     """Check if the object passed is undefined.  This does nothing more than
     performing an instance check against :class:`Undefined` but looks nicer.
diff --git a/tests/test_debug.py b/tests/test_debug.py
index d9c5f7a..aadb9a4 100644
--- a/tests/test_debug.py
+++ b/tests/test_debug.py
@@ -31,7 +31,19 @@
 >>> tmpl = MODULE.env.get_template('syntaxerror.html')
 Traceback (most recent call last):
   ...
-TemplateSyntaxError: unknown tag 'endif'
   File "loaderres/templates/syntaxerror.html", line 4
     {% endif %}
+TemplateSyntaxError: unknown tag 'endif'
+'''
+
+
+test_regular_syntax_error = '''
+>>> from jinja2.exceptions import TemplateSyntaxError
+>>> raise TemplateSyntaxError('wtf', 42)
+Traceback (most recent call last):
+  ...
+  File "<doctest test_regular_syntax_error[1]>", line 1, in <module>
+    raise TemplateSyntaxError('wtf', 42)
+TemplateSyntaxError: wtf
+  line 42
 '''