From adff499e47d99f6b40307acc1ace95508e3c5910 Mon Sep 17 00:00:00 2001 From: Preston Timmons Date: Fri, 24 Apr 2015 14:33:03 -0500 Subject: [PATCH] Fixed #24119, #24120 -- Formalized debug integration for template backends. --- django/template/backends/django.py | 30 +++- django/template/backends/dummy.py | 16 +- django/template/backends/jinja2.py | 52 ++++++- django/template/base.py | 24 ++- django/template/loader.py | 18 +-- django/views/debug.py | 28 +--- docs/releases/1.9.txt | 3 + docs/topics/_images/postmortem.png | Bin 0 -> 37075 bytes docs/topics/_images/template-lines.png | Bin 0 -> 21537 bytes docs/topics/templates.txt | 142 +++++++++++++++++- .../template_backends/syntax_error2.html | 31 ++++ tests/template_backends/test_dummy.py | 3 +- tests/template_backends/test_jinja2.py | 42 ++++++ tests/template_loader/tests.py | 28 +++- 14 files changed, 354 insertions(+), 63 deletions(-) create mode 100644 docs/topics/_images/postmortem.png create mode 100644 docs/topics/_images/template-lines.png create mode 100644 tests/template_backends/jinja2/template_backends/syntax_error2.html diff --git a/django/template/backends/django.py b/django/template/backends/django.py index 5940c0b457..9c2d24af13 100644 --- a/django/template/backends/django.py +++ b/django/template/backends/django.py @@ -1,11 +1,14 @@ # Since this package contains a "django" module, this is required on Python 2. from __future__ import absolute_import +import sys import warnings from django.conf import settings +from django.template import TemplateDoesNotExist from django.template.context import Context, RequestContext, make_context from django.template.engine import Engine, _dirs_undefined +from django.utils import six from django.utils.deprecation import RemovedInDjango20Warning from .base import BaseEngine @@ -24,21 +27,23 @@ class DjangoTemplates(BaseEngine): self.engine = Engine(self.dirs, self.app_dirs, **options) def from_string(self, template_code): - return Template(self.engine.from_string(template_code)) + return Template(self.engine.from_string(template_code), self) def get_template(self, template_name, dirs=_dirs_undefined): - return Template(self.engine.get_template(template_name, dirs)) + try: + return Template(self.engine.get_template(template_name, dirs), self) + except TemplateDoesNotExist as exc: + reraise(exc, self) class Template(object): - def __init__(self, template): + def __init__(self, template, backend): self.template = template + self.backend = backend @property def origin(self): - # TODO: define the Origin API. For now simply forwarding to the - # underlying Template preserves backwards-compatibility. return self.template.origin def render(self, context=None, request=None): @@ -71,4 +76,17 @@ class Template(object): else: context = make_context(context, request) - return self.template.render(context) + try: + return self.template.render(context) + except TemplateDoesNotExist as exc: + reraise(exc, self.backend) + + +def reraise(exc, backend): + """ + Reraise TemplateDoesNotExist while maintaining template debug information. + """ + new = exc.__class__(*exc.args, tried=exc.tried, backend=backend) + if hasattr(exc, 'template_debug'): + new.template_debug = exc.template_debug + six.reraise(exc.__class__, new, sys.exc_info()[2]) diff --git a/django/template/backends/dummy.py b/django/template/backends/dummy.py index 7df095baa6..1d6f446dec 100644 --- a/django/template/backends/dummy.py +++ b/django/template/backends/dummy.py @@ -1,12 +1,13 @@ # Since this package contains a "django" module, this is required on Python 2. from __future__ import absolute_import +import errno import io import string from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.template import TemplateDoesNotExist +from django.template import Origin, TemplateDoesNotExist from django.utils.html import conditional_escape from .base import BaseEngine @@ -29,17 +30,24 @@ class TemplateStrings(BaseEngine): return Template(template_code) def get_template(self, template_name): + tried = [] for template_file in self.iter_template_filenames(template_name): try: with io.open(template_file, encoding=settings.FILE_CHARSET) as fp: template_code = fp.read() - except IOError: - continue + except IOError as e: + if e.errno == errno.ENOENT: + tried.append(( + Origin(template_file, template_name, self), + 'Source does not exist', + )) + continue + raise return Template(template_code) else: - raise TemplateDoesNotExist(template_name) + raise TemplateDoesNotExist(template_name, tried=tried, backend=self) class Template(string.Template): diff --git a/django/template/backends/jinja2.py b/django/template/backends/jinja2.py index 0863773ddf..2d5190122a 100644 --- a/django/template/backends/jinja2.py +++ b/django/template/backends/jinja2.py @@ -41,17 +41,24 @@ class Jinja2(BaseEngine): try: return Template(self.env.get_template(template_name)) except jinja2.TemplateNotFound as exc: - six.reraise(TemplateDoesNotExist, TemplateDoesNotExist(exc.args), - sys.exc_info()[2]) + six.reraise( + TemplateDoesNotExist, + TemplateDoesNotExist(exc.name, backend=self), + sys.exc_info()[2], + ) except jinja2.TemplateSyntaxError as exc: - six.reraise(TemplateSyntaxError, TemplateSyntaxError(exc.args), - sys.exc_info()[2]) + new = TemplateSyntaxError(exc.args) + new.template_debug = get_exception_info(exc) + six.reraise(TemplateSyntaxError, new, sys.exc_info()[2]) class Template(object): def __init__(self, template): self.template = template + self.origin = Origin( + name=template.filename, template_name=template.name, + ) def render(self, context=None, request=None): if context is None: @@ -61,3 +68,40 @@ class Template(object): context['csrf_input'] = csrf_input_lazy(request) context['csrf_token'] = csrf_token_lazy(request) return self.template.render(context) + + +class Origin(object): + """ + A container to hold debug information as described in the template API + documentation. + """ + def __init__(self, name, template_name): + self.name = name + self.template_name = template_name + + +def get_exception_info(exception): + """ + Formats exception information for display on the debug page using the + structure described in the template API documentation. + """ + 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) + + return { + 'name': exception.filename, + 'message': exception.message, + 'source_lines': lines[top:bottom], + 'line': lineno, + 'before': '', + 'during': during, + 'after': '', + 'total': total, + 'top': top, + 'bottom': bottom, + } diff --git a/django/template/base.py b/django/template/base.py index e1849999f2..3f1e1c2d72 100644 --- a/django/template/base.py +++ b/django/template/base.py @@ -135,13 +135,27 @@ class TemplateSyntaxError(Exception): class TemplateDoesNotExist(Exception): """ - This exception is used when template loaders are unable to find a - template. The tried argument is an optional list of tuples containing - (origin, status), where origin is an Origin object and status is a string - with the reason the template wasn't found. + The exception used by backends when a template does not exist. Accepts the + following optional arguments: + + backend + The template backend class used when raising this exception. + + tried + A list of sources that were tried when finding the template. This + is formatted as a list of tuples containing (origin, status), where + origin is an Origin object and status is a string with the reason the + template wasn't found. + + chain + A list of intermediate TemplateDoesNotExist exceptions. This is used to + encapsulate multiple exceptions when loading templates from multiple + engines. """ - def __init__(self, msg, tried=None): + def __init__(self, msg, tried=None, backend=None, chain=None): + self.backend = backend self.tried = tried or [] + self.chain = chain or [] super(TemplateDoesNotExist, self).__init__(msg) diff --git a/django/template/loader.py b/django/template/loader.py index 2f9afd3071..43146e42b6 100644 --- a/django/template/loader.py +++ b/django/template/loader.py @@ -17,7 +17,7 @@ def get_template(template_name, dirs=_dirs_undefined, using=None): Raises TemplateDoesNotExist if no such template exists. """ - tried = [] + chain = [] engines = _engine_list(using) for engine in engines: try: @@ -33,9 +33,9 @@ def get_template(template_name, dirs=_dirs_undefined, using=None): else: return engine.get_template(template_name) except TemplateDoesNotExist as e: - tried.extend(e.tried) + chain.append(e) - raise TemplateDoesNotExist(template_name, tried=tried) + raise TemplateDoesNotExist(template_name, chain=chain) def select_template(template_name_list, dirs=_dirs_undefined, using=None): @@ -46,7 +46,7 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None): Raises TemplateDoesNotExist if no such template exists. """ - tried = [] + chain = [] engines = _engine_list(using) for template_name in template_name_list: for engine in engines: @@ -63,10 +63,10 @@ def select_template(template_name_list, dirs=_dirs_undefined, using=None): else: return engine.get_template(template_name) except TemplateDoesNotExist as e: - tried.extend(e.tried) + chain.append(e) if template_name_list: - raise TemplateDoesNotExist(', '.join(template_name_list), tried=tried) + raise TemplateDoesNotExist(', '.join(template_name_list), chain=chain) else: raise TemplateDoesNotExist("No template names provided") @@ -92,7 +92,7 @@ def render_to_string(template_name, context=None, return template.render(context, request) else: - tried = [] + chain = [] # Some deprecated arguments were passed - use the legacy code path for engine in _engine_list(using): try: @@ -124,13 +124,13 @@ def render_to_string(template_name, context=None, "method doesn't support the dictionary argument." % engine.name, stacklevel=2) except TemplateDoesNotExist as e: - tried.extend(e.tried) + chain.append(e) continue if template_name: if isinstance(template_name, (list, tuple)): template_name = ', '.join(template_name) - raise TemplateDoesNotExist(template_name, tried=tried) + raise TemplateDoesNotExist(template_name, chain=chain) else: raise TemplateDoesNotExist("No template names provided") diff --git a/django/views/debug.py b/django/views/debug.py index 9ae31d2dff..ae1352106a 100644 --- a/django/views/debug.py +++ b/django/views/debug.py @@ -9,7 +9,7 @@ from django.core.urlresolvers import Resolver404, resolve from django.http import ( HttpRequest, HttpResponse, HttpResponseNotFound, build_request_repr, ) -from django.template import Context, Engine, TemplateDoesNotExist, engines +from django.template import Context, Engine, TemplateDoesNotExist from django.template.defaultfilters import force_escape, pprint from django.utils import lru_cache, six, timezone from django.utils.datastructures import MultiValueDict @@ -276,25 +276,7 @@ class ExceptionReporter(object): """Return a dictionary containing traceback information.""" if self.exc_type and issubclass(self.exc_type, TemplateDoesNotExist): self.template_does_not_exist = True - postmortem = [] - - # TODO: add support for multiple template engines (#24120). - # TemplateDoesNotExist should carry all the information, including - # the backend, rather than looping through engines.all. - for engine in engines.all(): - if hasattr(engine, 'engine'): - e = engine.engine - else: - e = engine - - postmortem.append(dict( - engine=engine, - tried=[ - entry for entry in self.exc_value.tried if - entry[0].loader.engine == e - ], - )) - self.postmortem = postmortem + self.postmortem = self.exc_value.chain or [self.exc_value] frames = self.get_traceback_frames() for i, frame in enumerate(frames): @@ -751,7 +733,7 @@ TECHNICAL_500_TEMPLATE = (""" {% if postmortem %}

Django tried loading these templates, in this order:

{% for entry in postmortem %} -

Using engine {{ entry.engine.name }}:

+

Using engine {{ entry.backend.name }}: