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
This commit is contained in:
Adrian Holovaty 2005-11-23 23:10:17 +00:00
parent cfc5786d03
commit 5d863f1fbd
10 changed files with 454 additions and 239 deletions

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _
#################### ####################
DEBUG = False DEBUG = False
TEMPLATE_DEBUG = False
# Whether to use the "Etag" header. This saves bandwidth but slows down performance. # Whether to use the "Etag" header. This saves bandwidth but slows down performance.
USE_ETAGS = False USE_ETAGS = False

View File

@ -1,6 +1,7 @@
# Django settings for {{ project_name }} project. # Django settings for {{ project_name }} project.
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = ( ADMINS = (
# ('Your Name', 'your_email@domain.com'), # ('Your Name', 'your_email@domain.com'),

View File

@ -55,7 +55,7 @@ times with multiple contexts)
'\n<html>\n\n</html>\n' '\n<html>\n\n</html>\n'
""" """
import re import re
from django.conf.settings import DEFAULT_CHARSET from django.conf.settings import DEFAULT_CHARSET, TEMPLATE_DEBUG
__all__ = ('Template','Context','compile_string') __all__ = ('Template','Context','compile_string')
@ -74,6 +74,10 @@ VARIABLE_TAG_END = '}}'
ALLOWED_VARIABLE_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.' 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 # 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), 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))) 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" "Any function raising this exception will be ignored by resolve_variable"
pass 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: class Template:
def __init__(self, template_string): def __init__(self, template_string, origin=None):
"Compilation stage" "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): def __iter__(self):
for node in self.nodelist: for node in self.nodelist:
@ -115,10 +141,10 @@ class Template:
"Display stage -- can be called many times" "Display stage -- can be called many times"
return self.nodelist.render(context) return self.nodelist.render(context)
def compile_string(template_string): def compile_string(template_string, origin):
"Compiles template_string into NodeList ready for rendering" "Compiles template_string into NodeList ready for rendering"
tokens = tokenize(template_string) lexer = lexer_factory(template_string, origin)
parser = Parser(tokens) parser = parser_factory(lexer.tokenize())
return parser.parse() return parser.parse()
class Context: class Context:
@ -163,6 +189,12 @@ class Context:
return True return True
return False 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): def update(self, other_dict):
"Like dict.update(). Pushes an entire dictionary's keys and values onto the context." "Like dict.update(). Pushes an entire dictionary's keys and values onto the context."
self.dicts = [other_dict] + self.dicts self.dicts = [other_dict] + self.dicts
@ -174,39 +206,76 @@ class Token:
def __str__(self): def __str__(self):
return '<%s token: "%s...">' % ( 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', '') self.contents[:20].replace('\n', '')
) )
def tokenize(template_string): def __repr__(self):
"Return a list of tokens from a given template_string" return '<%s token: "%s">' % (
# remove all empty strings, because the regex has a tendency to add them {TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
bits = filter(None, tag_re.split(template_string)) self.contents[:].replace('\n', '')
return map(create_token, bits) )
def create_token(token_string): class Lexer(object):
"Convert the given token string into a new Token object and return it" def __init__(self, template_string, origin):
if token_string.startswith(VARIABLE_TAG_START): self.template_string = template_string
return Token(TOKEN_VAR, token_string[len(VARIABLE_TAG_START):-len(VARIABLE_TAG_END)].strip()) self.origin = origin
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)
class Parser: 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): def __init__(self, tokens):
self.tokens = tokens self.tokens = tokens
def parse(self, parse_until=[]): def parse(self, parse_until=[]):
nodelist = NodeList() nodelist = self.create_nodelist()
while self.tokens: while self.tokens:
token = self.next_token() token = self.next_token()
if token.token_type == TOKEN_TEXT: 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: elif token.token_type == TOKEN_VAR:
if not token.contents: if not token.contents:
raise TemplateSyntaxError, "Empty variable tag" self.empty_variable(token)
nodelist.append(VariableNode(token.contents)) var_node = self.create_variable_node(token.contents)
self.extend_nodelist(nodelist, var_node,token)
elif token.token_type == TOKEN_BLOCK: elif token.token_type == TOKEN_BLOCK:
if token.contents in parse_until: if token.contents in parse_until:
# put token back on token list so calling code knows why it terminated # put token back on token list so calling code knows why it terminated
@ -215,16 +284,57 @@ class Parser:
try: try:
command = token.contents.split()[0] command = token.contents.split()[0]
except IndexError: 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: try:
# execute callback function for this tag and append resulting node compile_func = registered_tags[command]
nodelist.append(registered_tags[command](self, token))
except KeyError: 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: if parse_until:
raise TemplateSyntaxError, "Unclosed tag(s): '%s'" % ', '.join(parse_until) self.unclosed_block_tag(parse_until)
return nodelist 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): def next_token(self):
return self.tokens.pop(0) return self.tokens.pop(0)
@ -234,6 +344,51 @@ class Parser:
def delete_first_token(self): def delete_first_token(self):
del self.tokens[0] 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: class TokenParser:
""" """
Subclass this and implement the top() method to parse a template line. When Subclass this and implement the top() method to parse a template line. When
@ -316,7 +471,34 @@ class TokenParser:
self.pointer = i self.pointer = i
return s 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), 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. 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 This class should never be instantiated outside of the
get_filters_from_token helper function. get_filters_from_token helper function.
""" """
def __init__(self, s): def __init__(self, token):
self.s = s matches = filter_re.finditer(token)
self.i = -1 var = None
self.current = '' filters = []
self.filters = [] upto = 0
self.current_filter_name = None for match in matches:
self.current_filter_arg = None start = match.start()
# First read the variable part. Decide whether we need to parse a if upto != start:
# string or a variable by peeking into the stream. raise TemplateSyntaxError, "Could not parse some characters: %s|%s|%s" % \
if self.peek_char() in ('_', '"', "'"): (token[:upto], token[upto:start], token[start:])
self.var = self.read_constant_string_token() if var == None:
else: var, constant, i18n_constant = match.group("var", "constant", "i18n_constant")
self.var = self.read_alphanumeric_token() if i18n_constant:
if not self.var: var = '"%s"' % _(i18n_constant)
raise TemplateSyntaxError, "Could not read variable name: '%s'" % self.s elif constant:
if self.var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or self.var[0] == '_': var = '"%s"' % constant
raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % self.var upto = match.end()
# Have we reached the end? if var == None:
if self.current is None: raise TemplateSyntaxError, "Could not find variable at start of %s" % token
return elif var.find(VARIABLE_ATTRIBUTE_SEPARATOR + '_') > -1 or var[0] == '_':
if self.current != FILTER_SEPARATOR: raise TemplateSyntaxError, "Variables and attributes may not begin with underscores: '%s'" % var
raise TemplateSyntaxError, "Bad character (expecting '%s') '%s'" % (FILTER_SEPARATOR, self.current) else:
# We have a filter separator; start reading the filters filter_name = match.group("filter_name")
self.read_filters() arg, i18n_arg = match.group("arg","i18n_arg")
if i18n_arg:
def peek_char(self): arg =_(i18n_arg.replace('\\', ''))
try: if arg:
return self.s[self.i+1] arg = arg.replace('\\', '')
except IndexError: if not registered_filters.has_key(filter_name):
return None raise TemplateSyntaxError, "Invalid filter: '%s'" % filter_name
if registered_filters[filter_name][1] == True and arg is None:
def next_char(self): raise TemplateSyntaxError, "Filter '%s' requires an argument" % filter_name
self.i = self.i + 1 if registered_filters[filter_name][1] == False and arg is not None:
try: raise TemplateSyntaxError, "Filter '%s' should not have an argument (argument is %r)" % (filter_name, arg)
self.current = self.s[self.i] filters.append( (filter_name,arg) )
except IndexError: upto = match.end()
self.current = None if upto != len(token):
raise TemplateSyntaxError, "Could not parse the remainder: %s" % token[upto:]
def read_constant_string_token(self): self.var , self.filters = var, filters
"""
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 get_filters_from_token(token): def get_filters_from_token(token):
"Convenient wrapper for FilterParser" "Convenient wrapper for FilterParser"
@ -580,7 +645,7 @@ class NodeList(list):
bits = [] bits = []
for node in self: for node in self:
if isinstance(node, Node): if isinstance(node, Node):
bits.append(node.render(context)) bits.append(self.render_node(node, context))
else: else:
bits.append(node) bits.append(node)
return ''.join(bits) return ''.join(bits)
@ -592,6 +657,25 @@ class NodeList(list):
nodes.extend(node.get_nodes_by_type(nodetype)) nodes.extend(node.get_nodes_by_type(nodetype))
return nodes 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): class TextNode(Node):
def __init__(self, s): def __init__(self, s):
self.s = s self.s = s
@ -609,14 +693,28 @@ class VariableNode(Node):
def __repr__(self): def __repr__(self):
return "<Variable Node: %s>" % self.var_string return "<Variable Node: %s>" % self.var_string
def render(self, context): def encode_output(self, output):
output = resolve_variable_with_filters(self.var_string, context)
# Check type so that we don't run str() on a Unicode object # Check type so that we don't run str() on a Unicode object
if not isinstance(output, basestring): if not isinstance(output, basestring):
output = str(output) return str(output)
elif isinstance(output, unicode): elif isinstance(output, unicode):
output = output.encode(DEFAULT_CHARSET) return output.encode(DEFAULT_CHARSET)
return output 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): def register_tag(token_command, callback_function):
registered_tags[token_command] = callback_function registered_tags[token_command] = callback_function

View File

@ -192,6 +192,7 @@ class RegroupNode(Node):
for obj in obj_list: for obj in obj_list:
grouper = resolve_variable_with_filters('var.%s' % self.expression, \ grouper = resolve_variable_with_filters('var.%s' % self.expression, \
Context({'var': obj})) Context({'var': obj}))
# TODO: Is this a sensible way to determine equality?
if output and repr(output[-1]['grouper']) == repr(grouper): if output and repr(output[-1]['grouper']) == repr(grouper):
output[-1]['list'].append(obj) output[-1]['list'].append(obj)
else: else:
@ -628,8 +629,8 @@ def do_load(parser, token):
# check at compile time that the module can be imported # check at compile time that the module can be imported
try: try:
LoadNode.load_taglib(taglib) LoadNode.load_taglib(taglib)
except ImportError: except ImportError, e:
raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e)
return LoadNode(taglib) return LoadNode(taglib)
def do_now(parser, token): def do_now(parser, token):

View File

@ -8,6 +8,10 @@
# name is the template name. # name is the template name.
# dirs is an optional list of directories to search instead of TEMPLATE_DIRS. # 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 # 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 # specifies whether the loader can be used in this Python installation. Each
# loader is responsible for setting this when it's initialized. # loader is responsible for setting this when it's initialized.
@ -17,8 +21,8 @@
# installed, because pkg_resources is necessary to read eggs. # installed, because pkg_resources is necessary to read eggs.
from django.core.exceptions import ImproperlyConfigured 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.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 from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG
template_source_loaders = [] template_source_loaders = []
for path in TEMPLATE_LOADERS: for path in TEMPLATE_LOADERS:
@ -38,14 +42,32 @@ for path in TEMPLATE_LOADERS:
else: else:
template_source_loaders.append(func) 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: for loader in template_source_loaders:
try: try:
return loader(name, dirs) source, display_name = loader(name, dirs)
return (source, make_origin(display_name, loader, name, dirs))
except TemplateDoesNotExist: except TemplateDoesNotExist:
pass pass
raise TemplateDoesNotExist, name raise TemplateDoesNotExist, name
def load_template_source(name, dirs=None):
find_template_source(name, dirs)[0]
class ExtendsError(Exception): class ExtendsError(Exception):
pass pass
@ -54,14 +76,14 @@ def get_template(template_name):
Returns a compiled Template object for the given template name, Returns a compiled Template object for the given template name,
handling template inheritance recursively. 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, Returns a compiled Template object for the given template code,
handling template inheritance recursively. handling template inheritance recursively.
""" """
return Template(source) return Template(source, origin)
def render_to_string(template_name, dictionary=None, context_instance=None): 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 error_msg += " Got this from the %r variable." % self.parent_name_var
raise TemplateSyntaxError, error_msg raise TemplateSyntaxError, error_msg
try: 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: except TemplateDoesNotExist:
raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent
@ -165,7 +187,9 @@ class ConstantIncludeNode(Node):
try: try:
t = get_template(template_path) t = get_template(template_path)
self.template = t self.template = t
except: except Exception, e:
if TEMPLATE_DEBUG:
raise
self.template = None self.template = None
def render(self, context): def render(self, context):
@ -183,6 +207,10 @@ class IncludeNode(Node):
template_name = resolve_variable(self.template_name, context) template_name = resolve_variable(self.template_name, context)
t = get_template(template_name) t = get_template(template_name)
return t.render(context) return t.render(context)
except TemplateSyntaxError, e:
if TEMPLATE_DEBUG:
raise
return ''
except: except:
return '' # Fail silently for invalid included templates. return '' # Fail silently for invalid included templates.
@ -236,6 +264,7 @@ def do_include(parser, token):
{% include "foo/some_include" %} {% include "foo/some_include" %}
""" """
bits = token.contents.split() bits = token.contents.split()
if len(bits) != 2: if len(bits) != 2:
raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0]

View File

@ -31,7 +31,7 @@ def load_template_source(template_name, template_dirs=None):
for template_dir in app_template_dirs: for template_dir in app_template_dirs:
filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
try: try:
return open(filepath).read() return (open(filepath).read(), filepath)
except IOError: except IOError:
pass pass
raise TemplateDoesNotExist, template_name raise TemplateDoesNotExist, template_name

View File

@ -18,7 +18,7 @@ def load_template_source(template_name, template_dirs=None):
pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION
for app in INSTALLED_APPS: for app in INSTALLED_APPS:
try: try:
return resource_string(app, pkg_name) return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name))
except: except:
pass pass
raise TemplateDoesNotExist, template_name raise TemplateDoesNotExist, template_name

View File

@ -11,7 +11,7 @@ def load_template_source(template_name, template_dirs=None):
for template_dir in template_dirs: for template_dir in template_dirs:
filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION
try: try:
return open(filepath).read() return (open(filepath).read(), filepath)
except IOError: except IOError:
tried.append(filepath) tried.append(filepath)
if template_dirs: if template_dirs:

View File

@ -1,19 +1,64 @@
import re
import os
import sys
import inspect
from django.conf import settings from django.conf import settings
from os.path import dirname, join as pathjoin
from django.core.template import Template, Context from django.core.template import Template, Context
from django.utils.html import escape
from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound 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') 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): def technical_500_response(request, exc_type, exc_value, tb):
""" """
Create a technical server error response. The last three arguments are Create a technical server error response. The last three arguments are
the values returned from sys.exc_info() and friends. 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 = [] frames = []
while tb is not None: while tb is not None:
filename = tb.tb_frame.f_code.co_filename 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 lineno = tb.tb_lineno - 1
pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7) pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7)
frames.append({ frames.append({
'tb' : tb, 'tb': tb,
'filename' : filename, 'filename': filename,
'function' : function, 'function': function,
'lineno' : lineno, 'lineno': lineno,
'vars' : tb.tb_frame.f_locals.items(), 'vars': tb.tb_frame.f_locals.items(),
'id' : id(tb), 'id': id(tb),
'pre_context' : pre_context, 'pre_context': pre_context,
'context_line' : context_line, 'context_line': context_line,
'post_context' : post_context, 'post_context': post_context,
'pre_context_lineno' : pre_context_lineno, 'pre_context_lineno': pre_context_lineno,
}) })
tb = tb.tb_next tb = tb.tb_next
@ -46,14 +91,14 @@ def technical_500_response(request, exc_type, exc_value, tb):
t = Template(TECHNICAL_500_TEMPLATE) t = Template(TECHNICAL_500_TEMPLATE)
c = Context({ c = Context({
'exception_type' : exc_type.__name__, 'exception_type': exc_type.__name__,
'exception_value' : exc_value, 'exception_value': exc_value,
'frames' : frames, 'frames': frames,
'lastframe' : frames[-1], 'lastframe': frames[-1],
'request' : request, 'request': request,
'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", 'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http",
'settings' : settings_dict, 'settings': settings_dict,
'template_info': template_info,
}) })
return HttpResponseServerError(t.render(c), mimetype='text/html') return HttpResponseServerError(t.render(c), mimetype='text/html')
@ -69,12 +114,12 @@ def technical_404_response(request, exception):
t = Template(TECHNICAL_404_TEMPLATE) t = Template(TECHNICAL_404_TEMPLATE)
c = Context({ c = Context({
'root_urlconf' : settings.ROOT_URLCONF, 'root_urlconf': settings.ROOT_URLCONF,
'urlpatterns' : tried, 'urlpatterns': tried,
'reason' : str(exception), 'reason': str(exception),
'request' : request, 'request': request,
'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", '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()]), 'settings': dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]),
}) })
return HttpResponseNotFound(t.render(c), mimetype='text/html') return HttpResponseNotFound(t.render(c), mimetype='text/html')
@ -144,6 +189,9 @@ TECHNICAL_500_TEMPLATE = """
#summary table { border:none; background:transparent; } #summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; } #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; } #requestinfo h3 { margin-bottom:-1em; }
table.source td { font-family: monospace; white-space: pre; }
span.specific { background:#ffcab7; }
.error { background: #ffc; }
</style> </style>
<script type="text/javascript"> <script type="text/javascript">
//<!-- //<!--
@ -221,7 +269,24 @@ TECHNICAL_500_TEMPLATE = """
</tr> </tr>
</table> </table>
</div> </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"> <div id="traceback">
<h2>Traceback <span>(innermost last)</span></h2> <h2>Traceback <span>(innermost last)</span></h2>
<ul class="traceback"> <ul class="traceback">

View File

@ -99,6 +99,9 @@ TEMPLATE_TESTS = {
# Chained filters, with an argument to the first one # Chained filters, with an argument to the first one
'basic-syntax29': ('{{ var|removetags:"b i"|upper|lower }}', {"var": "<b><i>Yes</i></b>"}, "yes"), '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 TAG ################################################################
'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"), 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),
'if-tag02': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": False}, "no"), '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 # 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), '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 ' # simple translation of a string delimited by '
'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"), 'i18n01': ("{% load i18n %}{% trans 'xxxyyyxxx' %}", {}, "xxxyyyxxx"),
@ -268,7 +288,7 @@ TEMPLATE_TESTS = {
def test_template_loader(template_name, template_dirs=None): def test_template_loader(template_name, template_dirs=None):
"A custom template loader that loads the unit-test templates." "A custom template loader that loads the unit-test templates."
try: try:
return TEMPLATE_TESTS[template_name][0] return ( TEMPLATE_TESTS[template_name][0] , "test:%s" % template_name )
except KeyError: except KeyError:
raise template.TemplateDoesNotExist, template_name 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) print "Template test: %s -- FAILED. Expected %r, got %r" % (name, vals[2], output)
failed_tests.append(name) failed_tests.append(name)
loader.template_source_loaders = old_template_loaders loader.template_source_loaders = old_template_loaders
deactivate()
if failed_tests and not standalone: if failed_tests and not standalone:
msg = "Template tests %s failed." % failed_tests msg = "Template tests %s failed." % failed_tests
if not verbosity: if not verbosity: