diff --git a/django/template/base.py b/django/template/base.py index 686a18ef2d..c008ac623d 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -69,7 +69,7 @@ from django.utils.encoding import ( force_str, force_text, python_2_unicode_compatible, ) from django.utils.formats import localize -from django.utils.html import conditional_escape +from django.utils.html import conditional_escape, escape from django.utils.itercompat import is_iterable from django.utils.module_loading import module_has_submodule from django.utils.safestring import ( @@ -187,12 +187,13 @@ class Template(object): if engine is None: from .engine import Engine engine = Engine.get_default() - if engine.debug and origin is None: + if origin is None: origin = StringOrigin(template_string) - self.nodelist = engine.compile_string(template_string, origin) self.name = name self.origin = origin self.engine = engine + self.source = template_string + self.nodelist = self.compile_nodelist() def __iter__(self): for node in self.nodelist: @@ -214,13 +215,139 @@ class Template(object): finally: context.render_context.pop() + def compile_nodelist(self): + """ + Parse and compile the template source into a nodelist. If debug + is True and an exception occurs during parsing, the exception is + is annotated with contextual line information where it occurred in the + template source. + """ + if self.engine.debug: + lexer = DebugLexer(self.source) + else: + lexer = Lexer(self.source) + + tokens = lexer.tokenize() + parser = Parser(tokens) + + try: + return parser.parse() + except Exception as e: + if self.engine.debug: + e.template_debug = self.get_exception_info(e, e.token) + raise + + def get_exception_info(self, exception, token): + """ + Return a dictionary containing contextual line information of where + the exception occurred in the template. The following information is + provided: + + message + The message of the exception raised. + + source_lines + The lines before, after, and including the line the exception + occurred on. + + line + The line number the exception occurred on. + + before, during, after + The line the exception occurred on split into three parts: + 1. The content before the token that raised the error. + 2. The token that raised the error. + 3. The content after the token that raised the error. + + total + The number of lines in source_lines. + + top + The line number where source_lines starts. + + bottom + The line number where source_lines ends. + + start + The start position of the token in the template source. + + end + The end position of the token in the template source. + """ + start, end = token.position + context_lines = 10 + line = 0 + upto = 0 + source_lines = [] + before = during = after = "" + for num, next in enumerate(linebreak_iter(self.source)): + if start >= upto and end <= next: + line = num + before = escape(self.source[upto:start]) + during = escape(self.source[start:end]) + after = escape(self.source[end:next]) + source_lines.append((num, escape(self.source[upto:next]))) + upto = next + total = len(source_lines) + + top = max(1, line - context_lines) + bottom = min(total, line + 1 + context_lines) + + # In some rare cases exc_value.args can be empty or an invalid + # unicode string. + try: + message = force_text(exception.args[0]) + except (IndexError, UnicodeDecodeError): + message = '(Could not get exception message)' + + return { + 'message': message, + 'source_lines': source_lines[top:bottom], + 'before': before, + 'during': during, + 'after': after, + 'top': top, + 'bottom': bottom, + 'total': total, + 'line': line, + 'name': self.origin.name, + 'start': start, + 'end': end, + } + + +def linebreak_iter(template_source): + yield 0 + p = template_source.find('\n') + while p >= 0: + yield p + 1 + p = template_source.find('\n', p + 1) + yield len(template_source) + 1 + class Token(object): - def __init__(self, token_type, contents): - # token_type must be TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK or - # TOKEN_COMMENT. + def __init__(self, token_type, contents, position=None, lineno=None): + """ + A token representing a string from the template. + + token_type + One of TOKEN_TEXT, TOKEN_VAR, TOKEN_BLOCK, or TOKEN_COMMENT. + + contents + The token source string. + + position + An optional tuple containing the start and end index of the token + in the template source. This is used for traceback information + when debug is on. + + lineno + The line number the token appears on in the template source. + This is used for traceback information and gettext files. + """ self.token_type, self.contents = token_type, contents - self.lineno = None + self.lineno = lineno + self.position = position def __str__(self): token_name = TOKEN_MAPPING[self.token_type] @@ -244,10 +371,8 @@ class Token(object): class Lexer(object): - def __init__(self, template_string, origin): + def __init__(self, template_string): self.template_string = template_string - self.origin = origin - self.lineno = 1 self.verbatim = False def tokenize(self): @@ -255,14 +380,16 @@ class Lexer(object): Return a list of tokens from a given template_string. """ in_tag = False + lineno = 1 result = [] for bit in tag_re.split(self.template_string): if bit: - result.append(self.create_token(bit, in_tag)) + result.append(self.create_token(bit, None, lineno, in_tag)) in_tag = not in_tag + lineno += bit.count('\n') return result - def create_token(self, token_string, in_tag): + def create_token(self, token_string, position, lineno, in_tag): """ Convert the given token string into a new Token object and return it. If in_tag is True, we are processing something that matched a tag, @@ -278,35 +405,69 @@ class Lexer(object): self.verbatim = False if in_tag and not self.verbatim: if token_string.startswith(VARIABLE_TAG_START): - token = Token(TOKEN_VAR, token_string[2:-2].strip()) + token = Token(TOKEN_VAR, token_string[2:-2].strip(), position, lineno) elif token_string.startswith(BLOCK_TAG_START): if block_content[:9] in ('verbatim', 'verbatim '): self.verbatim = 'end%s' % block_content - token = Token(TOKEN_BLOCK, block_content) + token = Token(TOKEN_BLOCK, block_content, position, lineno) elif token_string.startswith(COMMENT_TAG_START): content = '' if token_string.find(TRANSLATOR_COMMENT_MARK): content = token_string[2:-2].strip() - token = Token(TOKEN_COMMENT, content) + token = Token(TOKEN_COMMENT, content, position, lineno) else: - token = Token(TOKEN_TEXT, token_string) - token.lineno = self.lineno - self.lineno += token_string.count('\n') + token = Token(TOKEN_TEXT, token_string, position, lineno) return token +class DebugLexer(Lexer): + def tokenize(self): + """ + Split a template string into tokens and annotates each token with its + start and end position in the source. This is slower than the default + lexer so we only use it when debug is True. + """ + lineno = 1 + result = [] + upto = 0 + for match in tag_re.finditer(self.template_string): + start, end = match.span() + if start > upto: + token_string = self.template_string[upto:start] + result.append(self.create_token(token_string, (upto, start), lineno, in_tag=False)) + lineno += token_string.count('\n') + upto = start + token_string = self.template_string[start:end] + result.append(self.create_token(token_string, (start, end), lineno, in_tag=True)) + lineno += token_string.count('\n') + upto = end + last_bit = self.template_string[upto:] + if last_bit: + result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), lineno, in_tag=False)) + return result + + class Parser(object): def __init__(self, tokens): self.tokens = tokens self.tags = {} self.filters = {} + self.command_stack = [] for lib in builtins: self.add_library(lib) def parse(self, parse_until=None): + """ + Iterate through the parser tokens and compils each one into a node. + + If parse_until is provided, parsing will stop once one of the + specified tokens has been reached. This is formatted as a list of + tokens, e.g. ['elif', 'else', 'endif']. If no matching token is + reached, raise an exception with the unclosed block tag details. + """ if parse_until is None: parse_until = [] - nodelist = self.create_nodelist() + nodelist = NodeList() while self.tokens: token = self.next_token() # Use the raw values here for TOKEN_* for a tiny performance boost. @@ -314,38 +475,43 @@ class Parser(object): self.extend_nodelist(nodelist, TextNode(token.contents), token) elif token.token_type == 1: # TOKEN_VAR if not token.contents: - self.empty_variable(token) + raise self.error(token, 'Empty variable tag') try: filter_expression = self.compile_filter(token.contents) except TemplateSyntaxError as e: - if not self.compile_filter_error(token, e): - raise - var_node = self.create_variable_node(filter_expression) + raise self.error(token, e) + var_node = VariableNode(filter_expression) self.extend_nodelist(nodelist, var_node, token) elif token.token_type == 2: # TOKEN_BLOCK try: command = token.contents.split()[0] except IndexError: - self.empty_block_tag(token) + raise self.error(token, 'Empty block tag') if command in parse_until: - # put token back on token list so calling - # code knows why it terminated + # A matching token has been reached. Return control to + # the caller. Put the token back on the token list so the + # caller knows where it terminated. self.prepend_token(token) return nodelist - # execute callback function for this tag and append - # resulting node - self.enter_command(command, token) + # Add the token to the command stack. This is used for error + # messages if further parsing fails due to an unclosed block + # tag. + self.command_stack.append((command, token)) + # Get the tag callback function from the ones registered with + # the parser. try: compile_func = self.tags[command] except KeyError: self.invalid_block_tag(token, command, parse_until) + # Compile the callback into a node object and add it to + # the node list. try: compiled_result = compile_func(self, token) - except TemplateSyntaxError as e: - if not self.compile_function_error(token, e): - raise + except Exception as e: + raise self.error(token, e) self.extend_nodelist(nodelist, compiled_result, token) - self.exit_command() + # Compile success. Remove the token from the command stack. + self.command_stack.pop() if parse_until: self.unclosed_block_tag(parse_until) return nodelist @@ -357,38 +523,30 @@ class Parser(object): return self.unclosed_block_tag([endtag]) - def create_variable_node(self, filter_expression): - return VariableNode(filter_expression) - - def create_nodelist(self): - return NodeList() - def extend_nodelist(self, nodelist, node, token): - if node.must_be_first and nodelist: - try: - if nodelist.contains_nontext: - raise AttributeError - except AttributeError: - raise TemplateSyntaxError("%r must be the first tag " - "in the template." % node) + # Check that non-text nodes don't appear before an extends tag. + if node.must_be_first and nodelist.contains_nontext: + raise self.error( + token, '%r must be the first tag in the template.' % node, + ) if isinstance(nodelist, NodeList) and not isinstance(node, TextNode): nodelist.contains_nontext = True + # Set token here since we can't modify the node __init__ method + node.token = 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 error(self, token, e): + """ + Return an exception annotated with the originating token. Since the + parser can be called recursively, check if a token is already set. This + ensures the innermost token is highlighted if an exception occurs, + e.g. a compile error within the body of an if statement. + """ + if not isinstance(e, Exception): + e = TemplateSyntaxError(e) + if not hasattr(e, 'token'): + e.token = token + return e def invalid_block_tag(self, token, command, parse_until=None): if parse_until: @@ -397,13 +555,9 @@ class Parser(object): 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_filter_error(self, token, e): - pass - - def compile_function_error(self, token, e): - pass + command, token = self.command_stack.pop() + msg = "Unclosed tag '%s'. Looking for one of: %s." % (command, ', '.join(parse_until)) + raise self.error(token, msg) def next_token(self): return self.tokens.pop(0) @@ -752,6 +906,7 @@ class Node(object): # they can be preceded by text nodes. must_be_first = False child_nodelists = ('nodelist',) + token = None def render(self, context): """ @@ -759,6 +914,20 @@ class Node(object): """ pass + def render_annotated(self, context): + """ + Render the node. If debug is True and an exception occurs during + rendering, the exception is annotated with contextual line information + where it occurred in the template. For internal usage this method is + preferred over using the render method directly. + """ + try: + return self.render(context) + except Exception as e: + if context.template.engine.debug and not hasattr(e, 'template_debug'): + e.template_debug = context.template.get_exception_info(e, self.token) + raise + def __iter__(self): yield self @@ -786,7 +955,7 @@ class NodeList(list): bits = [] for node in self: if isinstance(node, Node): - bit = self.render_node(node, context) + bit = node.render_annotated(context) else: bit = node bits.append(force_text(bit)) @@ -799,9 +968,6 @@ class NodeList(list): nodes.extend(node.get_nodes_by_type(nodetype)) return nodes - def render_node(self, node, context): - return node.render(context) - class TextNode(Node): def __init__(self, s): diff --git a/django/template/debug.py b/django/template/debug.py deleted file mode 100644 index 75efdf9824..0000000000 --- a/django/template/debug.py +++ /dev/null @@ -1,102 +0,0 @@ -from django.template.base import ( - Lexer, NodeList, Parser, TemplateSyntaxError, VariableNode, tag_re, -) -from django.utils.encoding import force_text -from django.utils.formats import localize -from django.utils.html import conditional_escape -from django.utils.safestring import EscapeData, SafeData -from django.utils.timezone import template_localtime - - -class DebugLexer(Lexer): - def tokenize(self): - "Return a list of tokens from a given template_string" - result, upto = [], 0 - for match in tag_re.finditer(self.template_string): - start, end = match.span() - if start > upto: - result.append(self.create_token(self.template_string[upto:start], (upto, start), False)) - upto = start - result.append(self.create_token(self.template_string[start:end], (start, end), True)) - upto = end - last_bit = self.template_string[upto:] - if last_bit: - result.append(self.create_token(last_bit, (upto, upto + len(last_bit)), False)) - return result - - def create_token(self, token_string, source, in_tag): - token = super(DebugLexer, self).create_token(token_string, in_tag) - token.source = self.origin, source - return token - - -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.django_template_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_filter_error(self, token, e): - if not hasattr(e, 'django_template_source'): - e.django_template_source = token.source - - def compile_function_error(self, token, e): - if not hasattr(e, 'django_template_source'): - e.django_template_source = token.source - - -class DebugNodeList(NodeList): - def render_node(self, node, context): - try: - return node.render(context) - except Exception as e: - if not hasattr(e, 'django_template_source'): - e.django_template_source = node.source - raise - - -class DebugVariableNode(VariableNode): - def render(self, context): - try: - output = self.filter_expression.resolve(context) - output = template_localtime(output, use_tz=context.use_tz) - output = localize(output, use_l10n=context.use_l10n) - output = force_text(output) - except UnicodeDecodeError: - return '' - except Exception as e: - if not hasattr(e, 'django_template_source'): - e.django_template_source = self.source - raise - if (context.autoescape and not isinstance(output, SafeData)) or isinstance(output, EscapeData): - return conditional_escape(output) - else: - return output diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index a3b08a9120..e8e83ed46e 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -209,19 +209,10 @@ class ForNode(Node): context.update(unpacked_vars) else: context[self.loopvars[0]] = item - # In debug mode provide the source of the node which raised - # the exception - if context.template.engine.debug: - for node in self.nodelist_loop: - try: - nodelist.append(node.render(context)) - except Exception as e: - if not hasattr(e, 'django_template_source'): - e.django_template_source = node.source - raise - else: - for node in self.nodelist_loop: - nodelist.append(node.render(context)) + + for node in self.nodelist_loop: + nodelist.append(node.render_annotated(context)) + if pop_context: # The loop variables were pushed on to the context so pop them # off again. This is necessary because the tag lets the length diff --git a/django/template/engine.py b/django/template/engine.py index 9e7f88aeb4..ed398b217b 100644 --- a/django/template/engine.py +++ b/django/template/engine.py @@ -6,7 +6,7 @@ from django.utils.deprecation import RemovedInDjango20Warning from django.utils.functional import cached_property from django.utils.module_loading import import_string -from .base import Context, Lexer, Parser, Template, TemplateDoesNotExist +from .base import Context, Template, TemplateDoesNotExist from .context import _builtin_context_processors _context_instance_undefined = object() @@ -235,20 +235,6 @@ class Engine(object): # If we get here, none of the templates could be loaded raise TemplateDoesNotExist(', '.join(not_found)) - def compile_string(self, template_string, origin): - """ - Compiles template_string into a NodeList ready for rendering. - """ - if self.debug: - from .debug import DebugLexer, DebugParser - lexer_class, parser_class = DebugLexer, DebugParser - else: - lexer_class, parser_class = Lexer, Parser - lexer = lexer_class(template_string, origin) - tokens = lexer.tokenize() - parser = parser_class(tokens) - return parser.parse() - def make_origin(self, display_name, loader, name, dirs): if self.debug and display_name: # Inner import to avoid circular dependency diff --git a/django/template/loader.py b/django/template/loader.py index c8fb16bf92..182d9aacf6 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -16,9 +16,6 @@ class LoaderOrigin(Origin): 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 get_template(template_name, dirs=_dirs_undefined, using=None): """ diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index a857994132..fa100a390c 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -556,7 +556,7 @@ def templatize(src, origin=None): message = trim_whitespace(message) return message - for t in Lexer(src, origin).tokenize(): + for t in Lexer(src).tokenize(): if incomment: if t.token_type == TOKEN_BLOCK and t.contents == 'endcomment': content = ''.join(comment) diff --git a/django/views/debug.py b/django/views/debug.py index c2a290573a..1a5911fe20 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -15,7 +15,6 @@ from django.template.defaultfilters import force_escape, pprint from django.utils import lru_cache, six, timezone from django.utils.datastructures import MultiValueDict from django.utils.encoding import force_bytes, smart_text -from django.utils.html import escape from django.utils.module_loading import import_string from django.utils.translation import ugettext as _ @@ -265,7 +264,7 @@ class ExceptionReporter(object): self.tb = tb self.is_email = is_email - self.template_info = None + self.template_info = getattr(self.exc_value, 'template_debug', None) self.template_does_not_exist = False self.loader_debug_info = None @@ -323,12 +322,6 @@ class ExceptionReporter(object): 'templates': template_list, }) - # TODO: add support for multiple template engines (#24119). - if (default_template_engine is not None - and default_template_engine.debug - and hasattr(self.exc_value, 'django_template_source')): - self.get_template_exception_info() - frames = self.get_traceback_frames() for i, frame in enumerate(frames): if 'vars' in frame: @@ -393,46 +386,6 @@ class ExceptionReporter(object): c = Context(self.get_traceback_data(), autoescape=False, use_l10n=False) return t.render(c) - def get_template_exception_info(self): - origin, (start, end) = self.exc_value.django_template_source - template_source = origin.reload() - context_lines = 10 - line = 0 - upto = 0 - source_lines = [] - before = during = after = "" - for num, next in enumerate(linebreak_iter(template_source)): - 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]) - source_lines.append((num, escape(template_source[upto:next]))) - upto = next - total = len(source_lines) - - top = max(1, line - context_lines) - bottom = min(total, line + 1 + context_lines) - - # In some rare cases, exc_value.args might be empty. - try: - message = self.exc_value.args[0] - except IndexError: - message = '(Could not get exception message)' - - self.template_info = { - 'message': message, - 'source_lines': source_lines[top:bottom], - 'before': before, - 'during': during, - 'after': after, - 'top': top, - 'bottom': bottom, - 'total': total, - 'line': line, - 'name': origin.name, - } - def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None): """ Returns context_lines before and after lineno from file. diff --git a/tests/template_tests/syntax_tests/test_basic.py b/tests/template_tests/syntax_tests/test_basic.py index 741b3ace8f..b72d6e6414 100644 --- a/tests/template_tests/syntax_tests/test_basic.py +++ b/tests/template_tests/syntax_tests/test_basic.py @@ -311,3 +311,15 @@ class BasicSyntaxTests(SimpleTestCase): """ output = self.engine.render_to_string('basic-syntax38', {"var": {"callable": lambda: "foo bar"}}) self.assertEqual(output, 'foo bar') + + @setup({'template': '{% block content %}'}) + def test_unclosed_block(self): + msg = "Unclosed tag 'block'. Looking for one of: endblock." + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.render_to_string('template') + + @setup({'template': '{% if a %}'}) + def test_unclosed_block2(self): + msg = "Unclosed tag 'if'. Looking for one of: elif, else, endif." + with self.assertRaisesMessage(TemplateSyntaxError, msg): + self.engine.render_to_string('template') diff --git a/tests/template_tests/test_engine.py b/tests/template_tests/test_engine.py index 77f3c995b5..6b43fd9dd2 100644 --- a/tests/template_tests/test_engine.py +++ b/tests/template_tests/test_engine.py @@ -52,23 +52,11 @@ class DeprecatedRenderToStringTest(SimpleTestCase): class LoaderTests(SimpleTestCase): - def test_debug_nodelist_name(self): - engine = Engine(dirs=[TEMPLATE_DIR], debug=True) - template_name = 'index.html' - template = engine.get_template(template_name) - name = template.nodelist[0].source[0].name - self.assertTrue(name.endswith(template_name)) - def test_origin(self): engine = Engine(dirs=[TEMPLATE_DIR], debug=True) template = engine.get_template('index.html') self.assertEqual(template.origin.loadname, 'index.html') - def test_origin_debug_false(self): - engine = Engine(dirs=[TEMPLATE_DIR], debug=False) - template = engine.get_template('index.html') - self.assertEqual(template.origin, None) - def test_loader_priority(self): """ #21460 -- Check that the order of template loader works. diff --git a/tests/template_tests/test_loaders.py b/tests/template_tests/test_loaders.py index cdebbfb042..caa0b85db3 100644 --- a/tests/template_tests/test_loaders.py +++ b/tests/template_tests/test_loaders.py @@ -64,24 +64,6 @@ class CachedLoaderTests(SimpleTestCase): "Cached loader failed to cache the TemplateDoesNotExist exception", ) - def test_debug_nodelist_name(self): - template_name = 'index.html' - engine = Engine(dirs=[TEMPLATE_DIR], debug=True) - - template = engine.get_template(template_name) - name = template.nodelist[0].source[0].name - self.assertTrue( - name.endswith(template_name), - 'Template loaded through cached loader has incorrect name for debug page: %s' % template_name, - ) - - template = engine.get_template(template_name) - name = template.nodelist[0].source[0].name - self.assertTrue( - name.endswith(template_name), - 'Cached template loaded through cached loader has incorrect name for debug page: %s' % template_name, - ) - @unittest.skipUnless(pkg_resources, 'setuptools is not installed') class EggLoaderTests(SimpleTestCase): diff --git a/tests/template_tests/test_nodelist.py b/tests/template_tests/test_nodelist.py index 49e09833ed..fd21d828af 100644 --- a/tests/template_tests/test_nodelist.py +++ b/tests/template_tests/test_nodelist.py @@ -51,6 +51,5 @@ class ErrorIndexTest(TestCase): try: template.render(context) except (RuntimeError, TypeError) as e: - error_source_index = e.django_template_source[1] - self.assertEqual(error_source_index, - expected_error_source_index) + debug = e.template_debug + self.assertEqual((debug['start'], debug['end']), expected_error_source_index) diff --git a/tests/template_tests/test_parser.py b/tests/template_tests/test_parser.py index a88dec285e..dd22c83836 100644 --- a/tests/template_tests/test_parser.py +++ b/tests/template_tests/test_parser.py @@ -5,11 +5,10 @@ from __future__ import unicode_literals from unittest import TestCase -from django.template import Library, Template, TemplateSyntaxError +from django.template import Library, TemplateSyntaxError from django.template.base import ( TOKEN_BLOCK, FilterExpression, Parser, Token, Variable, ) -from django.test import override_settings from django.utils import six @@ -73,14 +72,6 @@ class ParserTests(TestCase): with six.assertRaisesRegex(self, TypeError, "Variable must be a string or number, got <(class|type) 'dict'>"): Variable({}) - @override_settings(DEBUG=True) - def test_compile_filter_error(self): - # regression test for #19819 - msg = "Could not parse the remainder: '@bar' from 'foo@bar'" - with six.assertRaisesRegex(self, TemplateSyntaxError, msg) as cm: - Template("{% if 1 %}{{ foo@bar }}{% endif %}") - self.assertEqual(cm.exception.django_template_source[1], (10, 23)) - def test_filter_args_count(self): p = Parser("") l = Library() diff --git a/tests/template_tests/tests.py b/tests/template_tests/tests.py index 9460cc3f1d..6fc6a03972 100644 --- a/tests/template_tests/tests.py +++ b/tests/template_tests/tests.py @@ -6,7 +6,7 @@ import sys from django.contrib.auth.models import Group from django.core import urlresolvers from django.template import ( - Context, Template, TemplateSyntaxError, engines, loader, + Context, Engine, Template, TemplateSyntaxError, engines, loader, ) from django.test import SimpleTestCase, override_settings @@ -48,31 +48,61 @@ class TemplateTests(SimpleTestCase): self.assertGreater(depth, 5, "The traceback context was lost when reraising the traceback. See #19827") - @override_settings(DEBUG=True) def test_no_wrapped_exception(self): """ # 16770 -- The template system doesn't wrap exceptions, but annotates them. """ + engine = Engine(debug=True) c = Context({"coconuts": lambda: 42 / 0}) - t = Template("{{ coconuts }}") - with self.assertRaises(ZeroDivisionError) as cm: + t = engine.from_string("{{ coconuts }}") + + with self.assertRaises(ZeroDivisionError) as e: t.render(c) - self.assertEqual(cm.exception.django_template_source[1], (0, 14)) + debug = e.exception.template_debug + self.assertEqual(debug['start'], 0) + self.assertEqual(debug['end'], 14) def test_invalid_block_suggestion(self): """ #7876 -- Error messages should include the unexpected block name. """ + engine = Engine() + with self.assertRaises(TemplateSyntaxError) as e: - Template("{% if 1 %}lala{% endblock %}{% endif %}") + engine.from_string("{% if 1 %}lala{% endblock %}{% endif %}") self.assertEqual( e.exception.args[0], "Invalid block tag: 'endblock', expected 'elif', 'else' or 'endif'", ) + def test_compile_filter_expression_error(self): + """ + 19819 -- Make sure the correct token is highlighted for + FilterExpression errors. + """ + engine = Engine(debug=True) + msg = "Could not parse the remainder: '@bar' from 'foo@bar'" + + with self.assertRaisesMessage(TemplateSyntaxError, msg) as e: + engine.from_string("{% if 1 %}{{ foo@bar }}{% endif %}") + + debug = e.exception.template_debug + self.assertEqual((debug['start'], debug['end']), (10, 23)) + self.assertEqual((debug['during']), '{{ foo@bar }}') + + def test_compile_tag_error(self): + """ + Errors raised while compiling nodes should include the token + information. + """ + engine = Engine(debug=True) + with self.assertRaises(RuntimeError) as e: + engine.from_string("{% load bad_tag %}{% badtag %}") + self.assertEqual(e.exception.template_debug['during'], '{% badtag %}') + def test_super_errors(self): """ #18169 -- NoReverseMatch should not be silence in block.super.