Commit 8414fcf1 authored by Tomáš Ehrlich's avatar Tomáš Ehrlich Committed by Tim Graham
Browse files

Fixes #23643 -- Added chained exception details to debug view.

parent ae87ad00
Loading
Loading
Loading
Loading
+52 −3
Original line number Diff line number Diff line
@@ -479,8 +479,29 @@ class ExceptionReporter(object):
        return lower_bound, pre_context, context_line, post_context

    def get_traceback_frames(self):
        def explicit_or_implicit_cause(exc_value):
            explicit = getattr(exc_value, '__cause__', None)
            implicit = getattr(exc_value, '__context__', None)
            return explicit or implicit

        # Get the exception and all its causes
        exceptions = []
        exc_value = self.exc_value
        while exc_value:
            exceptions.append(exc_value)
            exc_value = explicit_or_implicit_cause(exc_value)

        frames = []
        tb = self.tb
        # No exceptions were supplied to ExceptionReporter
        if not exceptions:
            return frames

        # In case there's just one exception (always in Python 2,
        # sometimes in Python 3), take the traceback from self.tb (Python 2
        # doesn't have a __traceback__ attribute on Exception)
        exc_value = exceptions.pop()
        tb = self.tb if not exceptions else exc_value.__traceback__

        while tb is not None:
            # Support for __traceback_hide__ which is used by a few libraries
            # to hide internal frames.
@@ -497,6 +518,8 @@ class ExceptionReporter(object):
            )
            if pre_context_lineno is not None:
                frames.append({
                    'exc_cause': explicit_or_implicit_cause(exc_value),
                    'exc_cause_explicit': getattr(exc_value, '__cause__', True),
                    'tb': tb,
                    'type': 'django' if module_name.startswith('django.') else 'user',
                    'filename': filename,
@@ -509,6 +532,13 @@ class ExceptionReporter(object):
                    'post_context': post_context,
                    'pre_context_lineno': pre_context_lineno + 1,
                })

            # If the traceback for current exception is consumed, try the
            # other exception.
            if not tb.tb_next and exceptions:
                exc_value = exceptions.pop()
                tb = exc_value.__traceback__
            else:
                tb = tb.tb_next

        return frames
@@ -838,6 +868,15 @@ TECHNICAL_500_TEMPLATE = ("""
  <div id="browserTraceback">
    <ul class="traceback">
      {% for frame in frames %}
        {% ifchanged frame.exc_cause %}{% if frame.exc_cause %}
          <li><h3>
          {% if frame.exc_cause_explicit %}
            The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception:
          {% else %}
            During handling of the above exception ({{ frame.exc_cause }}), another exception occurred:
          {% endif %}
        </h3></li>
        {% endif %}{% endifchanged %}
        <li class="frame {{ frame.type }}">
          <code>{{ frame.filename|escape }}</code> in <code>{{ frame.function|escape }}</code>

@@ -1123,7 +1162,17 @@ In template {{ template_info.name }}, error at line {{ template_info.line }}
   {{ source_line.0 }} : {{ source_line.1 }}
   {% endifequal %}{% endfor %}{% endif %}{% if frames %}
Traceback:
{% for frame in frames %}File "{{ frame.filename }}" in {{ frame.function }}
{% for frame in frames %}
{% ifchanged frame.exc_cause %}
  {% if frame.exc_cause %}
    {% if frame.exc_cause_explicit %}
      The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception:
    {% else %}
      During handling of the above exception ({{ frame.exc_cause }}), another exception occurred:
    {% endif %}
  {% endif %}
{% endifchanged %}
File "{{ frame.filename }}" in {{ frame.function }}
{% if frame.context_line %}  {{ frame.lineno }}. {{ frame.context_line }}{% endif %}
{% endfor %}
{% if exception_type %}Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}
+2 −0
Original line number Diff line number Diff line
@@ -172,6 +172,8 @@ Requests and Responses
  ``status_code`` outside of the constructor will also modify the value of
  ``reason_phrase``.

* The debug view now shows details of chained exceptions on Python 3.

Tests
^^^^^

+1 −1
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ doc_files = docs extras AUTHORS INSTALL LICENSE README.rst
install-script = scripts/rpm-install.sh

[flake8]
exclude = build,.git,./django/utils/lru_cache.py,./django/utils/six.py,./django/conf/app_template/*,./django/dispatch/weakref_backports.py,./tests/.env,./xmlrunner
exclude = build,.git,./django/utils/lru_cache.py,./django/utils/six.py,./django/conf/app_template/*,./django/dispatch/weakref_backports.py,./tests/.env,./xmlrunner,tests/view_tests/tests/py3_test_debug.py
ignore = E123,E128,E402,E501,W503,E731,W601
max-line-length = 119

+42 −0
Original line number Diff line number Diff line
"""
Since this file contains Python 3 specific syntax, it's named without a test_
prefix so the test runner won't try to import it. Instead, the test class is
imported in test_debug.py, but only on Python 3.

This filename is also in setup.cfg flake8 exclude since the Python 2 syntax
error (raise ... from ...) can't be silenced using NOQA.
"""
import sys

from django.test import RequestFactory, TestCase
from django.views.debug import ExceptionReporter


class Py3ExceptionReporterTests(TestCase):

    rf = RequestFactory()

    def test_reporting_of_nested_exceptions(self):
        request = self.rf.get('/test_view/')
        try:
            try:
                raise AttributeError('Top level')
            except AttributeError as explicit:
                try:
                    raise ValueError('Second exception') from explicit
                except ValueError:
                    raise IndexError('Final exception')
        except Exception:
            # Custom exception handler, just pass it into ExceptionReporter
            exc_type, exc_value, tb = sys.exc_info()

        explicit_exc = 'The above exception ({0}) was the direct cause of the following exception:'
        implicit_exc = 'During handling of the above exception ({0}), another exception occurred:'
        reporter = ExceptionReporter(request, exc_type, exc_value, tb)
        html = reporter.get_traceback_html()
        self.assertIn(explicit_exc.format("Top level"), html)
        self.assertIn(implicit_exc.format("Second exception"), html)

        text = reporter.get_traceback_text()
        self.assertIn(explicit_exc.format("Top level"), text)
        self.assertIn(implicit_exc.format("Second exception"), text)
+3 −0
Original line number Diff line number Diff line
@@ -28,6 +28,9 @@ from ..views import (
    sensitive_kwargs_function_caller, sensitive_method_view, sensitive_view,
)

if six.PY3:
    from .py3_test_debug import Py3ExceptionReporterTests  # NOQA


class CallableSettingWrapperTests(TestCase):
    """ Unittests for CallableSettingWrapper