diff options
-rw-r--r-- | examples/basic/debugger.py | 2 | ||||
-rw-r--r-- | examples/basic/templates/broken.html | 3 | ||||
-rw-r--r-- | examples/basic/templates/subbroken.html | 3 | ||||
-rw-r--r-- | jinja2/debug.py | 61 | ||||
-rw-r--r-- | jinja2/environment.py | 15 | ||||
-rw-r--r-- | jinja2/exceptions.py | 9 | ||||
-rw-r--r-- | jinja2/loaders.py | 3 | ||||
-rw-r--r-- | jinja2/runtime.py | 7 | ||||
-rw-r--r-- | jinja2/utils.py | 9 | ||||
-rw-r--r-- | tests/test_debug.py | 14 |
10 files changed, 98 insertions, 28 deletions
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 @@ from jinja2.loaders import FileSystemLoader 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 @@ def translate_exception(exc_info): 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 @@ def fake_exc_info(exc_info, filename, lineno, tb_back=None): 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 @@ def fake_exc_info(exc_info, filename, lineno, tb_back=None): # 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.compiler import generate 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 @@ class Environment(object): 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 @@ class Environment(object): 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 @@ class Environment(object): 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 @@ class Environment(object): 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 @@ class Environment(object): """ 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 @@ class TemplateSyntaxError(TemplateError): 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 @@ try: 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 @@ class BaseLoader(object): """ 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 @@ class Context(object): """ 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 @@ class BlockReference(object): 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 @@ class LoopContext(object): 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 @@ class Macro(object): 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 @@ class Undefined(object): 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 @@ _digits = '0123456789' # 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 @@ def environmentfunction(f): 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 @@ test_syntax_error = ''' >>> 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 ''' |