From 5d863f1fbd26537a8bca2920bc591279d15fbdf1 Mon Sep 17 00:00:00 2001 From: Adrian Holovaty Date: Wed, 23 Nov 2005 23:10:17 +0000 Subject: [PATCH] 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 --- django/conf/global_settings.py | 1 + django/conf/project_template/settings.py | 1 + django/core/template/__init__.py | 484 +++++++++++------- django/core/template/defaulttags.py | 5 +- django/core/template/loader.py | 47 +- .../core/template/loaders/app_directories.py | 2 +- django/core/template/loaders/eggs.py | 2 +- django/core/template/loaders/filesystem.py | 2 +- django/views/debug.py | 125 +++-- tests/othertests/templates.py | 24 +- 10 files changed, 454 insertions(+), 239 deletions(-) diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index a272f01970..565f72cfaf 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ #################### DEBUG = False +TEMPLATE_DEBUG = False # Whether to use the "Etag" header. This saves bandwidth but slows down performance. USE_ETAGS = False diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index eaeeb56a53..5135508f0f 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -1,6 +1,7 @@ # Django settings for {{ project_name }} project. DEBUG = True +TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@domain.com'), diff --git a/django/core/template/__init__.py b/django/core/template/__init__.py index c007e4bc80..5cb4e0a1c6 100644 --- a/django/core/template/__init__.py +++ b/django/core/template/__init__.py @@ -55,7 +55,7 @@ times with multiple contexts) '\n\n\n\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="" + # 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%(str)s)"%(i18n_close)s| +^"(?P%(str)s)"| +^(?P[%(var_chars)s]+)| + (?:%(filter_sep)s + (?P\w+) + (?:%(arg_sep)s + (?: + %(i18n_open)s"(?P%(str)s)"%(i18n_close)s| + "(?P%(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 "" % self.var_string - def render(self, context): - output = resolve_variable_with_filters(self.var_string, context) + def encode_output(self, output): # Check type so that we don't run str() on a Unicode object if not isinstance(output, basestring): - output = str(output) + return str(output) elif isinstance(output, unicode): - output = output.encode(DEFAULT_CHARSET) - return output + return output.encode(DEFAULT_CHARSET) + else: + return output + + def render(self, context): + output = resolve_variable_with_filters(self.var_string, context) + return self.encode_output(output) + +class DebugVariableNode(VariableNode): + def render(self, context): + try: + output = resolve_variable_with_filters(self.var_string, context) + except TemplateSyntaxError, e: + if not hasattr(e, 'source'): + e.source = self.source + raise + return self.encode_output(output) def register_tag(token_command, callback_function): registered_tags[token_command] = callback_function diff --git a/django/core/template/defaulttags.py b/django/core/template/defaulttags.py index 8997d54b79..08ae3d9852 100644 --- a/django/core/template/defaulttags.py +++ b/django/core/template/defaulttags.py @@ -192,6 +192,7 @@ class RegroupNode(Node): for obj in obj_list: grouper = resolve_variable_with_filters('var.%s' % self.expression, \ Context({'var': obj})) + # TODO: Is this a sensible way to determine equality? if output and repr(output[-1]['grouper']) == repr(grouper): output[-1]['list'].append(obj) else: @@ -628,8 +629,8 @@ def do_load(parser, token): # check at compile time that the module can be imported try: LoadNode.load_taglib(taglib) - except ImportError: - raise TemplateSyntaxError, "'%s' is not a valid tag library" % taglib + except ImportError, e: + raise TemplateSyntaxError, "'%s' is not a valid tag library: %s" % (taglib, e) return LoadNode(taglib) def do_now(parser, token): diff --git a/django/core/template/loader.py b/django/core/template/loader.py index 0369a35e2b..10989424db 100644 --- a/django/core/template/loader.py +++ b/django/core/template/loader.py @@ -8,6 +8,10 @@ # name is the template name. # dirs is an optional list of directories to search instead of TEMPLATE_DIRS. # +# The loader should return a tuple of (template_source, path). The path returned +# might be shown to the user for debugging purposes, so it should identify where +# the template was loaded from. +# # Each loader should have an "is_usable" attribute set. This is a boolean that # specifies whether the loader can be used in this Python installation. Each # loader is responsible for setting this when it's initialized. @@ -17,8 +21,8 @@ # installed, because pkg_resources is necessary to read eggs. from django.core.exceptions import ImproperlyConfigured -from django.core.template import Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag -from django.conf.settings import TEMPLATE_LOADERS +from django.core.template import Origin, StringOrigin, Template, Context, Node, TemplateDoesNotExist, TemplateSyntaxError, resolve_variable_with_filters, resolve_variable, register_tag +from django.conf.settings import TEMPLATE_LOADERS, TEMPLATE_DEBUG template_source_loaders = [] for path in TEMPLATE_LOADERS: @@ -38,14 +42,32 @@ for path in TEMPLATE_LOADERS: else: template_source_loaders.append(func) -def load_template_source(name, dirs=None): +class LoaderOrigin(Origin): + def __init__(self, display_name, loader, name, dirs): + super(LoaderOrigin, self).__init__(display_name) + self.loader, self.loadname, self.dirs = loader, name, dirs + + def reload(self): + return self.loader(self.loadname, self.dirs)[0] + +def make_origin(display_name, loader, name, dirs): + if TEMPLATE_DEBUG: + return LoaderOrigin(display_name, loader, name, dirs) + else: + return None + +def find_template_source(name, dirs=None): for loader in template_source_loaders: try: - return loader(name, dirs) + source, display_name = loader(name, dirs) + return (source, make_origin(display_name, loader, name, dirs)) except TemplateDoesNotExist: pass raise TemplateDoesNotExist, name +def load_template_source(name, dirs=None): + find_template_source(name, dirs)[0] + class ExtendsError(Exception): pass @@ -54,14 +76,14 @@ def get_template(template_name): Returns a compiled Template object for the given template name, handling template inheritance recursively. """ - return get_template_from_string(load_template_source(template_name)) + return get_template_from_string(*find_template_source(template_name)) -def get_template_from_string(source): +def get_template_from_string(source, origin=None ): """ Returns a compiled Template object for the given template code, handling template inheritance recursively. """ - return Template(source) + return Template(source, origin) def render_to_string(template_name, dictionary=None, context_instance=None): """ @@ -134,7 +156,7 @@ class ExtendsNode(Node): error_msg += " Got this from the %r variable." % self.parent_name_var raise TemplateSyntaxError, error_msg try: - return get_template_from_string(load_template_source(parent, self.template_dirs)) + return get_template_from_string(*find_template_source(parent, self.template_dirs)) except TemplateDoesNotExist: raise TemplateSyntaxError, "Template %r cannot be extended, because it doesn't exist" % parent @@ -165,7 +187,9 @@ class ConstantIncludeNode(Node): try: t = get_template(template_path) self.template = t - except: + except Exception, e: + if TEMPLATE_DEBUG: + raise self.template = None def render(self, context): @@ -183,6 +207,10 @@ class IncludeNode(Node): template_name = resolve_variable(self.template_name, context) t = get_template(template_name) return t.render(context) + except TemplateSyntaxError, e: + if TEMPLATE_DEBUG: + raise + return '' except: return '' # Fail silently for invalid included templates. @@ -236,6 +264,7 @@ def do_include(parser, token): {% include "foo/some_include" %} """ + bits = token.contents.split() if len(bits) != 2: raise TemplateSyntaxError, "%r tag takes one argument: the name of the template to be included" % bits[0] diff --git a/django/core/template/loaders/app_directories.py b/django/core/template/loaders/app_directories.py index b8bd0d6169..d7c02c68ea 100644 --- a/django/core/template/loaders/app_directories.py +++ b/django/core/template/loaders/app_directories.py @@ -31,7 +31,7 @@ def load_template_source(template_name, template_dirs=None): for template_dir in app_template_dirs: filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION try: - return open(filepath).read() + return (open(filepath).read(), filepath) except IOError: pass raise TemplateDoesNotExist, template_name diff --git a/django/core/template/loaders/eggs.py b/django/core/template/loaders/eggs.py index 33ba043220..5d48326dce 100644 --- a/django/core/template/loaders/eggs.py +++ b/django/core/template/loaders/eggs.py @@ -18,7 +18,7 @@ def load_template_source(template_name, template_dirs=None): pkg_name = 'templates/' + template_name + TEMPLATE_FILE_EXTENSION for app in INSTALLED_APPS: try: - return resource_string(app, pkg_name) + return (resource_string(app, pkg_name), 'egg:%s:%s ' % (app, pkg_name)) except: pass raise TemplateDoesNotExist, template_name diff --git a/django/core/template/loaders/filesystem.py b/django/core/template/loaders/filesystem.py index e5bb1bab1c..9a93481705 100644 --- a/django/core/template/loaders/filesystem.py +++ b/django/core/template/loaders/filesystem.py @@ -11,7 +11,7 @@ def load_template_source(template_name, template_dirs=None): for template_dir in template_dirs: filepath = os.path.join(template_dir, template_name) + TEMPLATE_FILE_EXTENSION try: - return open(filepath).read() + return (open(filepath).read(), filepath) except IOError: tried.append(filepath) if template_dirs: diff --git a/django/views/debug.py b/django/views/debug.py index 189b244af2..012d7f9a75 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -1,19 +1,64 @@ -import re -import os -import sys -import inspect from django.conf import settings -from os.path import dirname, join as pathjoin from django.core.template import Template, Context +from django.utils.html import escape from django.utils.httpwrappers import HttpResponseServerError, HttpResponseNotFound +import inspect, os, re, sys +from itertools import count, izip +from os.path import dirname, join as pathjoin HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD') +def linebreak_iter(template_source): + newline_re = re.compile("^", re.M) + for match in newline_re.finditer(template_source): + yield match.start() + yield len(template_source) + 1 + +def get_template_exception_info(exc_type, exc_value, tb): + origin, (start, end) = exc_value.source + template_source = origin.reload() + context_lines = 10 + line = 0 + upto = 0 + source_lines = [] + linebreaks = izip(count(0), linebreak_iter(template_source)) + linebreaks.next() # skip the nothing before initial line start + for num, next in linebreaks: + if start >= upto and end <= next: + line = num + before = escape(template_source[upto:start]) + during = escape(template_source[start:end]) + after = escape(template_source[end:next - 1]) + source_lines.append( (num, escape(template_source[upto:next - 1])) ) + upto = next + total = len(source_lines) + + top = max(0, line - context_lines) + bottom = min(total, line + 1 + context_lines) + + template_info = { + 'message': exc_value.args[0], + 'source_lines': source_lines[top:bottom], + 'before': before, + 'during': during, + 'after': after, + 'top': top , + 'bottom': bottom , + 'total': total, + 'line': line, + 'name': origin.name, + } + exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb) + return exc_info + (template_info,) + def technical_500_response(request, exc_type, exc_value, tb): """ Create a technical server error response. The last three arguments are the values returned from sys.exc_info() and friends. """ + template_info = None + if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'): + exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) frames = [] while tb is not None: filename = tb.tb_frame.f_code.co_filename @@ -21,16 +66,16 @@ def technical_500_response(request, exc_type, exc_value, tb): lineno = tb.tb_lineno - 1 pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7) frames.append({ - 'tb' : tb, - 'filename' : filename, - 'function' : function, - 'lineno' : lineno, - 'vars' : tb.tb_frame.f_locals.items(), - 'id' : id(tb), - 'pre_context' : pre_context, - 'context_line' : context_line, - 'post_context' : post_context, - 'pre_context_lineno' : pre_context_lineno, + 'tb': tb, + 'filename': filename, + 'function': function, + 'lineno': lineno, + 'vars': tb.tb_frame.f_locals.items(), + 'id': id(tb), + 'pre_context': pre_context, + 'context_line': context_line, + 'post_context': post_context, + 'pre_context_lineno': pre_context_lineno, }) tb = tb.tb_next @@ -46,14 +91,14 @@ def technical_500_response(request, exc_type, exc_value, tb): t = Template(TECHNICAL_500_TEMPLATE) c = Context({ - 'exception_type' : exc_type.__name__, - 'exception_value' : exc_value, - 'frames' : frames, - 'lastframe' : frames[-1], - 'request' : request, - 'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", - 'settings' : settings_dict, - + 'exception_type': exc_type.__name__, + 'exception_value': exc_value, + 'frames': frames, + 'lastframe': frames[-1], + 'request': request, + 'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http", + 'settings': settings_dict, + 'template_info': template_info, }) return HttpResponseServerError(t.render(c), mimetype='text/html') @@ -69,12 +114,12 @@ def technical_404_response(request, exception): t = Template(TECHNICAL_404_TEMPLATE) c = Context({ - 'root_urlconf' : settings.ROOT_URLCONF, - 'urlpatterns' : tried, - 'reason' : str(exception), - 'request' : request, - 'request_protocol' : os.environ.get("HTTPS") == "on" and "https" or "http", - 'settings' : dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]), + 'root_urlconf': settings.ROOT_URLCONF, + 'urlpatterns': tried, + 'reason': str(exception), + 'request': request, + 'request_protocol': os.environ.get("HTTPS") == "on" and "https" or "http", + 'settings': dict([(k, getattr(settings, k)) for k in dir(settings) if k.isupper()]), }) return HttpResponseNotFound(t.render(c), mimetype='text/html') @@ -144,6 +189,9 @@ TECHNICAL_500_TEMPLATE = """ #summary table { border:none; background:transparent; } #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; } #requestinfo h3 { margin-bottom:-1em; } + table.source td { font-family: monospace; white-space: pre; } + span.specific { background:#ffcab7; } + .error { background: #ffc; }