diff --git a/django/template/backends/jinja2.py b/django/template/backends/jinja2.py index ac7c05895c..d1e5217b51 100644 --- a/django/template/backends/jinja2.py +++ b/django/template/backends/jinja2.py @@ -1,3 +1,5 @@ +from pathlib import Path + import jinja2 from django.conf import settings @@ -68,7 +70,12 @@ class Template: context['csrf_token'] = csrf_token_lazy(request) for context_processor in self.backend.template_context_processors: context.update(context_processor(request)) - return self.template.render(context) + try: + return self.template.render(context) + except jinja2.TemplateSyntaxError as exc: + new = TemplateSyntaxError(exc.args) + new.template_debug = get_exception_info(exc) + raise new from exc class Origin: @@ -88,12 +95,22 @@ def get_exception_info(exception): """ context_lines = 10 lineno = exception.lineno - lines = list(enumerate(exception.source.strip().split("\n"), start=1)) - during = lines[lineno - 1][1] - total = len(lines) - top = max(0, lineno - context_lines - 1) - bottom = min(total, lineno + context_lines) - + source = exception.source + if source is None: + exception_file = Path(exception.filename) + if exception_file.exists(): + with open(exception_file, 'r') as fp: + source = fp.read() + if source is not None: + lines = list(enumerate(source.strip().split('\n'), start=1)) + during = lines[lineno - 1][1] + total = len(lines) + top = max(0, lineno - context_lines - 1) + bottom = min(total, lineno + context_lines) + else: + during = '' + lines = [] + total = top = bottom = 0 return { 'name': exception.filename, 'message': exception.message, diff --git a/tests/template_backends/jinja2/template_backends/syntax_error_include.html b/tests/template_backends/jinja2/template_backends/syntax_error_include.html new file mode 100644 index 0000000000..e23b0e78d3 --- /dev/null +++ b/tests/template_backends/jinja2/template_backends/syntax_error_include.html @@ -0,0 +1 @@ +{% include "template_backends/syntax_error.html" %} diff --git a/tests/template_backends/test_jinja2.py b/tests/template_backends/test_jinja2.py index a454e93a39..b908431914 100644 --- a/tests/template_backends/test_jinja2.py +++ b/tests/template_backends/test_jinja2.py @@ -1,5 +1,5 @@ from pathlib import Path -from unittest import skipIf +from unittest import mock, skipIf from django.template import TemplateSyntaxError from django.test import RequestFactory @@ -96,3 +96,39 @@ class Jinja2Tests(TemplateStringsTests): }) template = engine.get_template('hello.html') self.assertEqual(template.render({'name': 'Joe'}), 'Hello Joe!') + + def test_template_render_nested_error(self): + template = self.engine.get_template('template_backends/syntax_error_include.html') + with self.assertRaises(TemplateSyntaxError) as e: + template.render(context={}) + debug = e.exception.template_debug + self.assertEqual(debug['after'], '') + self.assertEqual(debug['before'], '') + self.assertEqual(debug['during'], '{% block %}') + self.assertEqual(debug['bottom'], 1) + self.assertEqual(debug['top'], 0) + self.assertEqual(debug['line'], 1) + self.assertEqual(debug['total'], 1) + self.assertEqual(len(debug['source_lines']), 1) + self.assertTrue(debug['name'].endswith('syntax_error.html')) + self.assertIn('message', debug) + + def test_template_render_error_nonexistent_source(self): + template = self.engine.get_template('template_backends/hello.html') + with mock.patch( + 'jinja2.environment.Template.render', + side_effect=jinja2.TemplateSyntaxError('', 1, filename='nonexistent.html'), + ): + with self.assertRaises(TemplateSyntaxError) as e: + template.render(context={}) + debug = e.exception.template_debug + self.assertEqual(debug['after'], '') + self.assertEqual(debug['before'], '') + self.assertEqual(debug['during'], '') + self.assertEqual(debug['bottom'], 0) + self.assertEqual(debug['top'], 0) + self.assertEqual(debug['line'], 1) + self.assertEqual(debug['total'], 0) + self.assertEqual(len(debug['source_lines']), 0) + self.assertTrue(debug['name'].endswith('nonexistent.html')) + self.assertIn('message', debug)