mirror of https://github.com/django/django.git
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:
parent
cfc5786d03
commit
5d863f1fbd
|
@ -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
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Django settings for {{ project_name }} project.
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@domain.com'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue