summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrian Holovaty <adrian@holovaty.com>2005-11-23 23:10:17 +0000
committerAdrian Holovaty <adrian@holovaty.com>2005-11-23 23:10:17 +0000
commit5d863f1fbd26537a8bca2920bc591279d15fbdf1 (patch)
tree44c640a364e95274e67a9555752b4561def7571a
parentcfc5786d03f3dc190f3ed1606edc3a196f16915e (diff)
downloaddjango-5d863f1fbd26537a8bca2920bc591279d15fbdf1.tar.gz
Fixed #603 -- Added template debugging errors to pretty error-page output, if TEMPLATE_DEBUG setting is True. Also refactored FilterParser for a significant speed increase and changed the template_loader interface so that it returns information about the loader. Taken from new-admin. Thanks rjwittams and crew
git-svn-id: http://code.djangoproject.com/svn/django/trunk@1379 bcc190cf-cafb-0310-a4f2-bffc1f526a37
-rw-r--r--django/conf/global_settings.py1
-rw-r--r--django/conf/project_template/settings.py1
-rw-r--r--django/core/template/__init__.py486
-rw-r--r--django/core/template/defaulttags.py5
-rw-r--r--django/core/template/loader.py47
-rw-r--r--django/core/template/loaders/app_directories.py2
-rw-r--r--django/core/template/loaders/eggs.py2
-rw-r--r--django/core/template/loaders/filesystem.py2
-rw-r--r--django/views/debug.py125
-rw-r--r--tests/othertests/templates.py24
10 files changed, 455 insertions, 240 deletions
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index a272f01970..565f72cfaf 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
####################
DEBUG = False
+TEMPLATE_DEBUG = False
# Whether to use the "Etag" header. This saves bandwidth but slows down performance.
USE_ETAGS = False
diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py
index eaeeb56a53..5135508f0f 100644
--- a/django/conf/project_template/settings.py
+++ b/django/conf/project_template/settings.py
@@ -1,6 +1,7 @@
# Django settings for {{ project_name }} project.
DEBUG = True
+TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
diff --git a/django/core/template/__init__.py b/django/core/template/__init__.py
index c007e4bc80..5cb4e0a1c6 100644
--- a/django/core/template/__init__.py
+++ b/django/core/template/__init__.py
@@ -55,7 +55,7 @@ times with multiple contexts)
'\n<html>\n\n</html>\n'
"""
import re
-from django.conf.settings import DEFAULT_CHARSET
+from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG
__all__ = ('Template','Context','compile_string')
@@ -74,6 +74,10 @@ VARIABLE_TAG_END = '}}'
ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.'
+# what to report as the origin for templates that come from non-loader sources
+# (e.g. strings)
+UNKNOWN_SOURCE="<unknown source>"
+
# match a variable or block tag and capture the entire tag, including start/end delimiters
tag_re = re.compile('(%s.*?%s|%s.*?%s)' % (re.escape(BLOCK_TAG_START), re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START), re.escape(VARIABLE_TAG_END)))
@@ -101,10 +105,32 @@ class SilentVariableFailure(Exception):
"Any function raising this exception will be ignored by resolve_variable"
pass
+class Origin(object):
+ def __init__(self, name):
+ self.name = name
+
+ def reload(self):
+ raise NotImplementedException
+
+ def __str__(self):
+ return self.name
+
+class StringOrigin(Origin):
+ def __init__(self, source):
+ super(StringOrigin, self).__init__(UNKNOWN_SOURCE)
+ self.source = source
+
+ def reload(self):
+ return self.source
+
class Template:
- def __init__(self, template_string):
+ def __init__(self, template_string, origin=None):
"Compilation stage"
- self.nodelist = compile_string(template_string)
+ if TEMPLATE_DEBUG and origin == None:
+ origin = StringOrigin(template_string)
+ # Could do some crazy stack-frame stuff to record where this string
+ # came from...
+ self.nodelist = compile_string(template_string, origin)
def __iter__(self):
for node in self.nodelist:
@@ -115,10 +141,10 @@ class Template:
"Display stage -- can be called many times"
return self.nodelist.render(context)
-def compile_string(template_string):
+def compile_string(template_string, origin):
"Compiles template_string into NodeList ready for rendering"
- tokens = tokenize(template_string)
- parser = Parser(tokens)
+ lexer = lexer_factory(template_string, origin)
+ parser = parser_factory(lexer.tokenize())
return parser.parse()
class Context:
@@ -163,6 +189,12 @@ class Context:
return True
return False
+ def get(self, key, otherwise):
+ for dict in self.dicts:
+ if dict.has_key(key):
+ return dict[key]
+ return otherwise
+
def update(self, other_dict):
"Like dict.update(). Pushes an entire dictionary's keys and values onto the context."
self.dicts = [other_dict] + self.dicts
@@ -174,39 +206,76 @@ class Token:
def __str__(self):
return '<%s token: "%s...">' % (
- {TOKEN_TEXT:'Text', TOKEN_VAR:'Var', TOKEN_BLOCK:'Block'}[self.token_type],
+ {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
self.contents[:20].replace('\n', '')
)
-def tokenize(template_string):
- "Return a list of tokens from a given template_string"
- # remove all empty strings, because the regex has a tendency to add them
- bits = filter(None, tag_re.split(template_string))
- return map(create_token, bits)
-
-def create_token(token_string):
- "Convert the given token string into a new Token object and return it"
- if token_string.startswith(VARIABLE_TAG_START):
- return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
- elif token_string.startswith(BLOCK_TAG_START):
- return Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
- else:
- return Token(TOKEN_TEXT, token_string)
+ def __repr__(self):
+ return '<%s token: "%s">' % (
+ {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
+ self.contents[:].replace('\n', '')
+ )
-class Parser:
+class Lexer(object):
+ def __init__(self, template_string, origin):
+ self.template_string = template_string
+ self.origin = origin
+
+ def tokenize(self):
+ "Return a list of tokens from a given template_string"
+ # remove all empty strings, because the regex has a tendency to add them
+ bits = filter(None, tag_re.split(self.template_string))
+ return map(self.create_token, bits)
+
+ def create_token(self,token_string):
+ "Convert the given token string into a new Token object and return it"
+ if token_string.startswith(VARIABLE_TAG_START):
+ token = Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip())
+ elif token_string.startswith(BLOCK_TAG_START):
+ token = Token(TOKEN_BLOCK, token_string[len(BLOCK_TAG_START):-len(BLOCK_TAG_END)].strip())
+ else:
+ token = Token(TOKEN_TEXT, token_string)
+ return token
+
+class DebugLexer(Lexer):
+ def __init__(self, template_string, origin):
+ super(DebugLexer, self).__init__(template_string, origin)
+
+ def tokenize(self):
+ "Return a list of tokens from a given template_string"
+ token_tups, upto = [], 0
+ for match in tag_re.finditer(self.template_string):
+ start, end = match.span()
+ if start > upto:
+ token_tups.append( (self.template_string[upto:start], (upto, start)) )
+ upto = start
+ token_tups.append( (self.template_string[start:end], (start,end)) )
+ upto = end
+ last_bit = self.template_string[upto:]
+ if last_bit:
+ token_tups.append( (last_bit, (upto, upto + len(last_bit))) )
+ return [self.create_token(tok, (self.origin, loc)) for tok, loc in token_tups]
+
+ def create_token(self, token_string, source):
+ token = super(DebugLexer, self).create_token(token_string)
+ token.source = source
+ return token
+
+class Parser(object):
def __init__(self, tokens):
self.tokens = tokens
def parse(self, parse_until=[]):
- nodelist = NodeList()
+ nodelist = self.create_nodelist()
while self.tokens:
token = self.next_token()
if token.token_type == TOKEN_TEXT:
- nodelist.append(TextNode(token.contents))
+ self.extend_nodelist(nodelist, TextNode(token.contents), token)
elif token.token_type == TOKEN_VAR:
if not token.contents:
- raise TemplateSyntaxError, "Empty variable tag"
- nodelist.append(VariableNode(token.contents))
+ self.empty_variable(token)
+ var_node = self.create_variable_node(token.contents)
+ self.extend_nodelist(nodelist, var_node,token)
elif token.token_type == TOKEN_BLOCK:
if token.contents in parse_until:
# put token back on token list so calling code knows why it terminated
@@ -215,16 +284,57 @@ class Parser:
try:
command = token.contents.split()[0]
except IndexError:
- raise TemplateSyntaxError, "Empty block tag"
+ self.empty_block_tag(token)
+ # execute callback function for this tag and append resulting node
+ self.enter_command(command, token)
try:
- # execute callback function for this tag and append resulting node
- nodelist.append(registered_tags[command](self, token))
+ compile_func = registered_tags[command]
except KeyError:
- raise TemplateSyntaxError, "Invalid block tag: '%s'" % command
+ self.invalid_block_tag(token, command)
+ try:
+ compiled_result = compile_func(self, token)
+ except TemplateSyntaxError, e:
+ if not self.compile_function_error(token, e):
+ raise
+ self.extend_nodelist(nodelist, compiled_result, token)
+ self.exit_command()
if parse_until:
- raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until)
+ self.unclosed_block_tag(parse_until)
return nodelist
+ def create_variable_node(self, contents):
+ return VariableNode(contents)
+
+ def create_nodelist(self):
+ return NodeList()
+
+ def extend_nodelist(self, nodelist, node, token):
+ nodelist.append(node)
+
+ def enter_command(self, command, token):
+ pass
+
+ def exit_command(self):
+ pass
+
+ def error(self, token, msg ):
+ return TemplateSyntaxError(msg)
+
+ def empty_variable(self, token):
+ raise self.error( token, "Empty variable tag")
+
+ def empty_block_tag(self, token):
+ raise self.error( token, "Empty block tag")
+
+ def invalid_block_tag(self, token, command):
+ raise self.error( token, "Invalid block tag: '%s'" % command)
+
+ def unclosed_block_tag(self, parse_until):
+ raise self.error(None, "Unclosed tags: %s " % ', '.join(parse_until))
+
+ def compile_function_error(self, token, e):
+ pass
+
def next_token(self):
return self.tokens.pop(0)
@@ -234,6 +344,51 @@ class Parser:
def delete_first_token(self):
del self.tokens[0]
+class DebugParser(Parser):
+ def __init__(self, lexer):
+ super(DebugParser, self).__init__(lexer)
+ self.command_stack = []
+
+ def enter_command(self, command, token):
+ self.command_stack.append( (command, token.source) )
+
+ def exit_command(self):
+ self.command_stack.pop()
+
+ def error(self, token, msg):
+ return self.source_error(token.source, msg)
+
+ def source_error(self, source,msg):
+ e = TemplateSyntaxError(msg)
+ e.source = source
+ return e
+
+ def create_nodelist(self):
+ return DebugNodeList()
+
+ def create_variable_node(self, contents):
+ return DebugVariableNode(contents)
+
+ def extend_nodelist(self, nodelist, node, token):
+ node.source = token.source
+ super(DebugParser, self).extend_nodelist(nodelist, node, token)
+
+ def unclosed_block_tag(self, parse_until):
+ (command, source) = self.command_stack.pop()
+ msg = "Unclosed tag '%s'. Looking for one of: %s " % (command, ', '.join(parse_until))
+ raise self.source_error( source, msg)
+
+ def compile_function_error(self, token, e):
+ if not hasattr(e, 'source'):
+ e.source = token.source
+
+if TEMPLATE_DEBUG:
+ lexer_factory = DebugLexer
+ parser_factory = DebugParser
+else:
+ lexer_factory = Lexer
+ parser_factory = Parser
+
class TokenParser:
"""
Subclass this and implement the top() method to parse a template line. When
@@ -316,7 +471,34 @@ class TokenParser:
self.pointer = i
return s
-class FilterParser:
+
+
+
+filter_raw_string = r"""
+^%(i18n_open)s"(?P<i18n_constant>%(str)s)"%(i18n_close)s|
+^"(?P<constant>%(str)s)"|
+^(?P<var>[%(var_chars)s]+)|
+ (?:%(filter_sep)s
+ (?P<filter_name>\w+)
+ (?:%(arg_sep)s
+ (?:
+ %(i18n_open)s"(?P<i18n_arg>%(str)s)"%(i18n_close)s|
+ "(?P<arg>%(str)s)"
+ )
+ )?
+ )""" % {
+ 'str': r"""[^"\\]*(?:\\.[^"\\]*)*""",
+ 'var_chars': "A-Za-z0-9\_\." ,
+ 'filter_sep': re.escape(FILTER_SEPARATOR),
+ 'arg_sep': re.escape(FILTER_ARGUMENT_SEPARATOR),
+ 'i18n_open' : re.escape("_("),
+ 'i18n_close' : re.escape(")"),
+ }
+
+filter_raw_string = filter_raw_string.replace("\n", "").replace(" ", "")
+filter_re = re.compile(filter_raw_string)
+
+class FilterParser(object):
"""
Parses a variable token and its optional filters (all as a single string),
and return a list of tuples of the filter name and arguments.
@@ -331,162 +513,45 @@ class FilterParser:
This class should never be instantiated outside of the
get_filters_from_token helper function.
"""
- def __init__(self, s):
- self.s = s
- self.i = -1
- self.current = ''
- self.filters = []
- self.current_filter_name = None
- self.current_filter_arg = None
- # First read the variable part. Decide whether we need to parse a
- # string or a variable by peeking into the stream.
- if self.peek_char() in ('_', '"', "'"):
- self.var = self.read_constant_string_token()
- else:
- self.var = self.read_alphanumeric_token()
- if not self.var:
- raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s
- if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_':
- raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var
- # Have we reached the end?
- if self.current is None:
- return
- if self.current != FILTER_SEPARATOR:
- raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)
- # We have a filter separator; start reading the filters
- self.read_filters()
-
- def peek_char(self):
- try:
- return self.s[self.i+1]
- except IndexError:
- return None
-
- def next_char(self):
- self.i = self.i + 1
- try:
- self.current = self.s[self.i]
- except IndexError:
- self.current = None
-
- def read_constant_string_token(self):
- """
- Reads a constant string that must be delimited by either " or '
- characters. The string is returned with its delimiters.
- """
- val = ''
- qchar = None
- i18n = False
- self.next_char()
- if self.current == '_':
- i18n = True
- self.next_char()
- if self.current != '(':
- raise TemplateSyntaxError, "Bad character (expecting '(') '%s'" % self.current
- self.next_char()
- if not self.current in ('"', "'"):
- raise TemplateSyntaxError, "Bad character (expecting '\"' or ''') '%s'" % self.current
- qchar = self.current
- val += qchar
- while 1:
- self.next_char()
- if self.current == qchar:
- break
- val += self.current
- val += self.current
- self.next_char()
- if i18n:
- if self.current != ')':
- raise TemplateSyntaxError, "Bad character (expecting ')') '%s'" % self.current
- self.next_char()
- val = qchar+_(val.strip(qchar))+qchar
- return val
-
- def read_alphanumeric_token(self):
- """
- Reads a variable name or filter name, which are continuous strings of
- alphanumeric characters + the underscore.
- """
- var = ''
- while 1:
- self.next_char()
- if self.current is None:
- break
- if self.current not in ALLOWED_VARIABLE_CHARS:
- break
- var += self.current
- return var
-
- def read_filters(self):
- while 1:
- filter_name, arg = self.read_filter()
- if not registered_filters.has_key(filter_name):
- raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name
- if registered_filters[filter_name][1] == True and arg is None:
- raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name
- if registered_filters[filter_name][1] == False and arg is not None:
- raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg)
- self.filters.append((filter_name, arg))
- if self.current is None:
- break
-
- def read_filter(self):
- self.current_filter_name = self.read_alphanumeric_token()
- self.current_filter_arg = None
- # Have we reached the end?
- if self.current is None:
- return (self.current_filter_name, None)
- # Does the filter have an argument?
- if self.current == FILTER_ARGUMENT_SEPARATOR:
- self.current_filter_arg = self.read_arg()
- return (self.current_filter_name, self.current_filter_arg)
- # Next thing MUST be a pipe
- if self.current != FILTER_SEPARATOR:
- raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current)
- return (self.current_filter_name, self.current_filter_arg)
-
- def read_arg(self):
- # First read a " or a _("
- self.next_char()
- translated = False
- if self.current == '_':
- self.next_char()
- if self.current != '(':
- raise TemplateSyntaxError, "Bad character (expecting '(') '%s'" % self.current
- translated = True
- self.next_char()
- if self.current != '"':
- raise TemplateSyntaxError, "Bad character (expecting '\"') '%s'" % self.current
- self.escaped = False
- arg = ''
- while 1:
- self.next_char()
- if self.current == '"' and not self.escaped:
- break
- if self.current == '\\' and not self.escaped:
- self.escaped = True
- continue
- if self.current == '\\' and self.escaped:
- arg += '\\'
- self.escaped = False
- continue
- if self.current == '"' and self.escaped:
- arg += '"'
- self.escaped = False
- continue
- if self.escaped and self.current not in '\\"':
- raise TemplateSyntaxError, "Unescaped backslash in '%s'" % self.s
- if self.current is None:
- raise TemplateSyntaxError, "Unexpected end of argument in '%s'" % self.s
- arg += self.current
- # self.current must now be '"'
- self.next_char()
- if translated:
- if self.current != ')':
- raise TemplateSyntaxError, "Bad character (expecting ')') '%s'" % self.current
- self.next_char()
- arg = _(arg)
- return arg
+ def __init__(self, token):
+ matches = filter_re.finditer(token)
+ var = None
+ filters = []
+ upto = 0
+ for match in matches:
+ start = match.start()
+ if upto != start:
+ raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s" % \
+ (token[:upto], token[upto:start], token[start:])
+ if var == None:
+ var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
+ if i18n_constant:
+ var = '"%s"' % _(i18n_constant)
+ elif constant:
+ var = '"%s"' % constant
+ upto = match.end()
+ if var == None:
+ raise TemplateSyntaxError, "Could not find variable at start of %s" % token
+ elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
+ raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var
+ else:
+ filter_name = match.group("filter_name")
+ arg, i18n_arg = match.group("arg","i18n_arg")
+ if i18n_arg:
+ arg =_(i18n_arg.replace('\\', ''))
+ if arg:
+ arg = arg.replace('\\', '')
+ if not registered_filters.has_key(filter_name):
+ raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name
+ if registered_filters[filter_name][1] == True and arg is None:
+ raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name
+ if registered_filters[filter_name][1] == False and arg is not None:
+ raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg)
+ filters.append( (filter_name,arg) )
+ upto = match.end()
+ if upto != len(token):
+ raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:]
+ self.var , self.filters = var, filters
def get_filters_from_token(token):
"Convenient wrapper for FilterParser"
@@ -580,7 +645,7 @@ class NodeList(list):
bits = []
for node in self:
if isinstance(node, Node):
- bits.append(node.render(context))
+ bits.append(self.render_node(node, context))
else:
bits.append(node)
return ''.join(bits)
@@ -592,6 +657,25 @@ class NodeList(list):
nodes.extend(node.get_nodes_by_type(nodetype))
return nodes
+ def render_node(self, node, context):
+ return(node.render(context))
+
+class DebugNodeList(NodeList):
+ def render_node(self, node, context):
+ try:
+ result = node.render(context)
+ except TemplateSyntaxError, e:
+ if not hasattr(e, 'source'):
+ e.source = node.source
+ raise
+ except Exception:
+ from sys import exc_info
+ wrapped = TemplateSyntaxError('Caught an exception while rendering.')
+ wrapped.source = node.source
+ wrapped.exc_info = exc_info()
+ raise wrapped
+ return result
+
class TextNode(Node):
def __init__(self, s):
self.s = s
@@ -609,14 +693,28 @@ class VariableNode(Node):
def __repr__(self):
return "<Variable Node: %s>" % self.var_string
- def render(self, context):
- output = resolve_variable_with_filters(self.var_string, context)
+ def encode_output(self, output):
# Check type so that we don't run str() on a Unicode object
if not isinstance(output, basestring):
- output = str(output)
+ return str(output)
elif isinstance(output, unicode):
- output = output.encode(DEFAULT_CHARSET)
- return output
+ return output.encode(DEFAULT_CHARSET)
+ else:
+ return output
+
+ def render(self, context):
+ output = resolve_variable_with_filters(self.var_string, context)
+ return self.encode_output(output)
+
+class DebugVariableNode(VariableNode):
+ def render(self, context):
+ try:
+ output = resolve_variable_with_filters(self.var_string, context)
+ except TemplateSyntaxError, e:
+ if not hasattr(e, 'source'):
+ e.source = self.source
+ raise
+ return self.encode_output(output)
def register_tag(token_command, callback_function):
registered_tags[token_command] = callback_function
diff --git a/django/core/template/defaulttags.py b/django/core/template/defaulttags.py
index 8997d54b79..08ae3d9852 100644
--- a/django/core/template/defaulttags.py
+++ b/django/core/template/defaulttags.py
@@ -192,6 +192,7 @@ class RegroupNode(Node):
for obj in obj_list:
grouper = resolve_variable_with_filters('var.%s' % self.expression, \
Context({'var': obj}))
+ # TODO: Is this a sensible way to determine equality?
if output and repr(output[-1]['grouper']) == repr(grouper):
output[-1]['list'].append(obj)
else:
@@ -628,8 +629,8 @@ def do_load(parser, token):
# check at compile time that the module can be imported
try:
LoadNode.load_taglib(taglib)
- except ImportError:
- raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib
+ except ImportError, e:
+ raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e)
return LoadNode(taglib)
def do_now(parser, token):
diff --git a/django/core/template/loader.py b/django/core/template/loader.py
index 0369a35e2b..10989424db 100644
--- a/django/core/template/loader.py
+++ b/django/core/template/loader.py
@@ -8,6 +8,10 @@
# name is the template name.
# dirs is an optional list of directories to search instead of TEMPLATE_DIRS.
#
+# The loader should return a tuple of (template_source, path). The path returned
+# might be shown to the user for debugging purposes, so it should identify where
+# the template was loaded from.
+#
# Each loader should have an "is_usable" attribute set. This is a boolean that
# specifies whether the loader can be used in this Python installation. Each
# loader is responsible for setting this when it's initialized.
@@ -17,8 +21,8 @@
# installed, because pkg_resources is necessary to read eggs.
from django.core.exceptions import ImproperlyConfigured
-from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag
-from django.conf.settings import TEMPLATE_LOADERS
+from django.core.template import Origin, StringOrigin, Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag
+from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG
template_source_loaders = []
for path in TEMPLATE_LOADERS:
@@ -38,14 +42,32 @@ for path in TEMPLATE_LOADERS:
else:
template_source_loaders.append(func)
-def load_template_source(name, dirs=None):
+class LoaderOrigin(Origin):
+ def __init__(self, display_name, loader, name, dirs):
+ super(LoaderOrigin, self).__init__(display_name)
+ self.loader, self.loadname, self.dirs = loader, name, dirs
+
+ def reload(self):
+ return self.loader(self.loadname, self.dirs)[0]
+
+def make_origin(display_name, loader, name, dirs):
+ if TEMPLATE_DEBUG:
+ return LoaderOrigin(display_name, loader, name, dirs)
+ else:
+ return None
+
+def find_template_source(name, dirs=None):
for loader in template_source_loaders:
try:
- return loader(name, dirs)
+ source, display_name = loader(name, dirs)
+ return (source, make_origin(display_name, loader, name, dirs))
except TemplateDoesNotExist:
pass
raise TemplateDoesNotExist, name
+def load_template_source(name, dirs=None):
+ find_template_source(name, dirs)[0]
+
class ExtendsError(Exception):
pass
@@ -54,14 +76,14 @@ def get_template(template_name):
Returns a compiled Template object for the given template name,
handling template inheritance recursively.
"""
- return get_template_from_string(load_template_source(template_name))
+ return get_template_from_string(*find_template_source(template_name))
-def get_template_from_string(source):
+def get_template_from_string(source, origin=None ):
"""
Returns a compiled Template object for the given template code,
handling template inheritance recursively.
"""
- return Template(source)
+ return Template(source, origin)
def render_to_string(template_name, dictionary=None, context_instance=None):
"""
@@ -134,7 +156,7 @@ class ExtendsNode(Node):
error_msg += " Got this from the %r variable." % self.parent_name_var
raise TemplateSyntaxError, error_msg
try:
- return get_template_from_string(load_template_source(parent, self.template_dirs))
+ return get_template_from_string(*find_template_source(parent, self.template_dirs))
except TemplateDoesNotExist:
raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
@@ -165,7 +187,9 @@ class ConstantIncludeNode(Node):
try:
t = get_template(template_path)
self.template = t
- except:
+ except Exception, e:
+ if TEMPLATE_DEBUG:
+ raise
self.template = None
def render(self, context):
@@ -183,6 +207,10 @@ class IncludeNode(Node):
template_name = resolve_variable(self.template_name, context)
t = get_template(template_name)
return t.render(context)
+ except TemplateSyntaxError, e:
+ if TEMPLATE_DEBUG:
+ raise
+ return ''
except:
return '' # Fail silently for invalid included templates.
@@ -236,6 +264,7 @@ def do_include(parser, token):
{% include "foo/some_include" %}
"""
+
bits = token.contents.split()
if len(bits) != 2:
raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0]
diff --git a/django/core/template/loaders/app_directories.py b/django/core/template/loaders/app_directories.py
index b8bd0d6169..d7c02c68ea 100644
--- a/django/core/template/loaders/app_directories.py
+++ b/django/core/template/loaders/app_directories.py
@@ -31,7 +31,7 @@ def load_template_source(template_name, template_dirs=None):
for template_dir in app_template_dirs:
filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
try:
- return open(filepath).read()
+ return (open(filepath).read(), filepath)
except IOError:
pass
raise TemplateDoesNotExist, template_name
diff --git a/django/core/template/loaders/eggs.py b/django/core/template/loaders/eggs.py
index 33ba043220..5d48326dce 100644
--- a/django/core/template/loaders/eggs.py
+++ b/django/core/template/loaders/eggs.py
@@ -18,7 +18,7 @@ def load_template_source(template_name, template_dirs=None):
pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION
for app in INSTALLED_APPS:
try:
- return resource_string(app, pkg_name)
+ return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name))
except:
pass
raise TemplateDoesNotExist, template_name
diff --git a/django/core/template/loaders/filesystem.py b/django/core/template/loaders/filesystem.py
index e5bb1bab1c..9a93481705 100644
--- a/django/core/template/loaders/filesystem.py
+++ b/django/core/template/loaders/filesystem.py
@@ -11,7 +11,7 @@ def load_template_source(template_name, template_dirs=None):
for template_dir in template_dirs:
filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
try:
- return open(filepath).read()
+ return (open(filepath).read(), filepath)
except IOError:
tried.append(filepath)
if template_dirs:
diff --git a/django/views/debug.py b/django/views/debug.py
index 189b244af2..012d7f9a75 100644
--- a/django/views/debug.py
+++ b/django/views/debug.py
@@ -1,19 +1,64 @@
-import re
-import os
-import sys
-import inspect
from django.conf import settings
-from os.path import dirname, join as pathjoin
from django.core.template import Template, Context
+from django.utils.html import escape
from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound
+import inspect, os, re, sys
+from itertools import count, izip
+from os.path import dirname, join as pathjoin
HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD')
+def linebreak_iter(template_source):
+ newline_re = re.compile("^", re.M)
+ for match in newline_re.finditer(template_source):
+ yield match.start()
+ yield len(template_source) + 1
+
+def get_template_exception_info(exc_type, exc_value, tb):
+ origin, (start, end) = exc_value.source
+ template_source = origin.reload()
+ context_lines = 10
+ line = 0
+ upto = 0
+ source_lines = []
+ linebreaks = izip(count(0), linebreak_iter(template_source))
+ linebreaks.next() # skip the nothing before initial line start
+ for num, next in linebreaks:
+ if start >= upto and end <= next:
+ line = num
+ before = escape(template_source[upto:start])
+ during = escape(template_source[start:end])
+ after = escape(template_source[end:next - 1])
+ source_lines.append( (num, escape(template_source[upto:next - 1])) )
+ upto = next
+ total = len(source_lines)
+
+ top = max(0, line - context_lines)
+ bottom = min(total, line + 1 + context_lines)
+
+ template_info = {
+ 'message': exc_value.args[0],
+ 'source_lines': source_lines[top:bottom],
+ 'before': before,
+ 'during': during,
+ 'after': after,
+ 'top': top ,
+ 'bottom': bottom ,
+ 'total': total,
+ 'line': line,
+ 'name': origin.name,
+ }
+ exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb)
+ return exc_info + (template_info,)
+
def technical_500_response(request, exc_type, exc_value, tb):
"""
Create a technical server error response. The last three arguments are
the values returned from sys.exc_info() and friends.
"""
+ template_info = None
+ if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'):
+ exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb)
frames = []
while tb is not None:
filename = tb.tb_frame.f_code.co_filename
@@ -21,16 +66,16 @@ def technical_500_response(request, exc_type, exc_value, tb):
lineno = tb.tb_lineno - 1
pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7)
frames.append({
- 'tb' : tb,
- 'filename' : filename,
- 'function' : function,
- 'lineno' : lineno,
- 'vars' : tb.tb_frame.f_locals.items(),
- 'id' : id(tb),
- 'pre_context' : pre_context,
- 'context_line' : context_line,
- 'post_context' : post_context,
- 'pre_context_lineno' : pre_context_lineno,
+ 'tb': tb,
+ 'filename': filename,
+ 'function': function,
+ 'lineno': lineno,
+ 'vars': tb.tb_frame.f_locals.items(),
+ 'id': id(tb),
+ 'pre_context': pre_context,
+ 'context_line': context_line,
+ 'post_context': post_context,
+ 'pre_context_lineno': pre_context_lineno,
})
tb = tb.tb_next
@@ -46,14 +91,14 @@ def technical_500_response(request, exc_type, exc_value, tb):
t = Template(TECHNICAL_500_TEMPLATE)
c = Context({
- 'exception_type' : exc_type.__name__,
- 'exception_value' : exc_value,
- 'frames' : frames,
- 'lastframe' : frames[-1],
- 'request' : request,
- 'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http",
- 'settings' : settings_dict,
-
+ 'exception_type': exc_type.__name__,
+ 'exception_value': exc_value,
+ 'frames': frames,
+ 'lastframe': frames[-1],
+ 'request': request,
+ 'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http",
+ 'settings': settings_dict,
+ 'template_info': template_info,
})
return HttpResponseServerError(t.render(c), mimetype='text/html')
@@ -69,12 +114,12 @@ def technical_404_response(request, exception):
t = Template(TECHNICAL_404_TEMPLATE)
c = Context({
- 'root_urlconf' : settings.ROOT_URLCONF,
- 'urlpatterns' : tried,
- 'reason' : str(exception),
- 'request' : request,
- 'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http",
- 'settings' : dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]),
+ 'root_urlconf': settings.ROOT_URLCONF,
+ 'urlpatterns': tried,
+ 'reason': str(exception),
+ 'request': request,
+ 'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http",
+ 'settings': dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]),
})
return HttpResponseNotFound(t.render(c), mimetype='text/html')
@@ -144,6 +189,9 @@ TECHNICAL_500_TEMPLATE = """
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
+ table.source td { font-family: monospace; white-space: pre; }
+ span.specific { background:#ffcab7; }
+ .error { background: #ffc; }
</style>
<script type="text/javascript">
//<!--
@@ -221,7 +269,24 @@ TECHNICAL_500_TEMPLATE = """
</tr>
</table>
</div>
-
+{% if template_info %}
+<div id="template">
+ <h2>Template</h2>
+ In template {{ template_info.name }}, error at line {{ template_info.line }}
+ <div>{{ template_info.message|escape }}</div>
+ <table class="source{% if template_info.top %} cut-top{% endif %}{% ifnotequal template_info.bottom template_info.total %} cut-bottom{% endifnotequal %}">
+ {% for source_line in template_info.source_lines %}
+ {% ifequal source_line.0 template_info.line %}
+ <tr class="error"><td>{{ source_line.0 }}</td>
+ <td>{{ template_info.before }}<span class="specific">{{ template_info.during }}</span>{{ template_info.after }}</td></tr>
+ {% else %}
+ <tr><td>{{ source_line.0 }}</td>
+ <td> {{ source_line.1 }}</td></tr>
+ {% endifequal %}
+ {% endfor %}
+ </table>
+</div>
+{% endif %}
<div id="traceback">
<h2>Traceback <span>(innermost last)</span></h2>
<ul class="traceback">
diff --git a/tests/othertests/templates.py b/tests/othertests/templates.py
index 1211cde86b..60d3627708 100644
--- a/tests/othertests/templates.py
+++ b/tests/othertests/templates.py
@@ -99,6 +99,9 @@ TEMPLATE_TESTS = {
# Chained filters, with an argument to the first one
'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"),
+ #Escaped string as argument
+ 'basic-syntax30': (r"""{{ var|default_if_none:" endquote\" hah" }}""", {"var": None}, ' endquote" hah'),
+
### IF TAG ################################################################
'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),
'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"),
@@ -225,6 +228,23 @@ TEMPLATE_TESTS = {
# Raise exception for custom tags used in child with {% load %} tag in parent, not in child
'exception04': ("{% extends 'inheritance17' %}{% block first %}{% echo 400 %}5678{% endblock %}", {}, template.TemplateSyntaxError),
+ 'multiline01': ("""
+ Hello,
+ boys.
+ How
+ are
+ you
+ gentlemen.
+ """,
+ {},
+ """
+ Hello,
+ boys.
+ How
+ are
+ you
+ gentlemen.
+ """ ),
# simple translation of a string delimited by '
'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"),
@@ -268,7 +288,7 @@ TEMPLATE_TESTS = {
def test_template_loader(template_name, template_dirs=None):
"A custom template loader that loads the unit-test templates."
try:
- return TEMPLATE_TESTS[template_name][0]
+ return ( TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name )
except KeyError:
raise template.TemplateDoesNotExist, template_name
@@ -308,7 +328,7 @@ def run_tests(verbosity=0, standalone=False):
print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output)
failed_tests.append(name)
loader.template_source_loaders = old_template_loaders
-
+ deactivate()
if failed_tests and not standalone:
msg = "Template tests %s failed." % failed_tests
if not verbosity: