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
TEMPLATE_DEBUG = False
# Whether to use the "Etag" header. This saves bandwidth but slows down performance.
USE_ETAGS = False

View File

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

View File

@ -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 __repr__(self):
return '<%s token: "%s">' % (
{TOKEN_TEXT: 'Text', TOKEN_VAR: 'Var', TOKEN_BLOCK: 'Block'}[self.token_type],
self.contents[:].replace('\n', '')
)
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)
class Lexer(object):
def __init__(self, template_string, origin):
self.template_string = template_string
self.origin = origin
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):
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

View File

@ -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):

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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">

View File

@ -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: