1
0
mirror of https://github.com/django/django.git synced 2025-10-24 22:26:08 +00:00

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

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