Sfoglia il codice sorgente

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

Tomáš Ehrlich 10 anni fa
parent
commit
8414fcf16b

+ 52 - 3
django/views/debug.py

@@ -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,7 +532,14 @@ class ExceptionReporter(object):
                     'post_context': post_context,
                     'pre_context_lineno': pre_context_lineno + 1,
                 })
-            tb = tb.tb_next
+
+            # 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
docs/releases/1.9.txt

@@ -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
setup.cfg

@@ -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
tests/view_tests/tests/py3_test_debug.py

@@ -0,0 +1,42 @@
+"""
+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
tests/view_tests/tests/test_debug.py

@@ -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