diff --git a/django/views/debug.py b/django/views/debug.py index d0235f4bd4..c2a290573a 100644 --- a/django/views/debug.py +++ b/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 = ("""
{{ frame.filename|escape }}
in {{ frame.function|escape }}
@@ -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 %}
diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt
index 1be30ae561..a6544a6030 100644
--- a/docs/releases/1.9.txt
+++ b/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
^^^^^
diff --git a/setup.cfg b/setup.cfg
index 40440291de..365dc627b9 100644
--- a/setup.cfg
+++ b/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
diff --git a/tests/view_tests/tests/py3_test_debug.py b/tests/view_tests/tests/py3_test_debug.py
new file mode 100644
index 0000000000..38d840f07a
--- /dev/null
+++ b/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)
diff --git a/tests/view_tests/tests/test_debug.py b/tests/view_tests/tests/test_debug.py
index 51f86f225f..95dfb8f875 100644
--- a/tests/view_tests/tests/test_debug.py
+++ b/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