Skip to content

Commit 8414fcf

Browse files
tricoder42timgraham
authored andcommitted
Fixes #23643 -- Added chained exception details to debug view.
1 parent ae87ad0 commit 8414fcf

File tree

5 files changed

+100
-4
lines changed

5 files changed

+100
-4
lines changed

django/views/debug.py

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,8 +479,29 @@ def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, mod
479479
return lower_bound, pre_context, context_line, post_context
480480

481481
def get_traceback_frames(self):
482+
def explicit_or_implicit_cause(exc_value):
483+
explicit = getattr(exc_value, '__cause__', None)
484+
implicit = getattr(exc_value, '__context__', None)
485+
return explicit or implicit
486+
487+
# Get the exception and all its causes
488+
exceptions = []
489+
exc_value = self.exc_value
490+
while exc_value:
491+
exceptions.append(exc_value)
492+
exc_value = explicit_or_implicit_cause(exc_value)
493+
482494
frames = []
483-
tb = self.tb
495+
# No exceptions were supplied to ExceptionReporter
496+
if not exceptions:
497+
return frames
498+
499+
# In case there's just one exception (always in Python 2,
500+
# sometimes in Python 3), take the traceback from self.tb (Python 2
501+
# doesn't have a __traceback__ attribute on Exception)
502+
exc_value = exceptions.pop()
503+
tb = self.tb if not exceptions else exc_value.__traceback__
504+
484505
while tb is not None:
485506
# Support for __traceback_hide__ which is used by a few libraries
486507
# to hide internal frames.
@@ -497,6 +518,8 @@ def get_traceback_frames(self):
497518
)
498519
if pre_context_lineno is not None:
499520
frames.append({
521+
'exc_cause': explicit_or_implicit_cause(exc_value),
522+
'exc_cause_explicit': getattr(exc_value, '__cause__', True),
500523
'tb': tb,
501524
'type': 'django' if module_name.startswith('django.') else 'user',
502525
'filename': filename,
@@ -509,7 +532,14 @@ def get_traceback_frames(self):
509532
'post_context': post_context,
510533
'pre_context_lineno': pre_context_lineno + 1,
511534
})
512-
tb = tb.tb_next
535+
536+
# If the traceback for current exception is consumed, try the
537+
# other exception.
538+
if not tb.tb_next and exceptions:
539+
exc_value = exceptions.pop()
540+
tb = exc_value.__traceback__
541+
else:
542+
tb = tb.tb_next
513543

514544
return frames
515545

@@ -838,6 +868,15 @@ def default_urlconf(request):
838868
<div id="browserTraceback">
839869
<ul class="traceback">
840870
{% for frame in frames %}
871+
{% ifchanged frame.exc_cause %}{% if frame.exc_cause %}
872+
<li><h3>
873+
{% if frame.exc_cause_explicit %}
874+
The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception:
875+
{% else %}
876+
During handling of the above exception ({{ frame.exc_cause }}), another exception occurred:
877+
{% endif %}
878+
</h3></li>
879+
{% endif %}{% endifchanged %}
841880
<li class="frame {{ frame.type }}">
842881
<code>{{ frame.filename|escape }}</code> in <code>{{ frame.function|escape }}</code>
843882
@@ -1123,7 +1162,17 @@ def default_urlconf(request):
11231162
{{ source_line.0 }} : {{ source_line.1 }}
11241163
{% endifequal %}{% endfor %}{% endif %}{% if frames %}
11251164
Traceback:
1126-
{% for frame in frames %}File "{{ frame.filename }}" in {{ frame.function }}
1165+
{% for frame in frames %}
1166+
{% ifchanged frame.exc_cause %}
1167+
{% if frame.exc_cause %}
1168+
{% if frame.exc_cause_explicit %}
1169+
The above exception ({{ frame.exc_cause }}) was the direct cause of the following exception:
1170+
{% else %}
1171+
During handling of the above exception ({{ frame.exc_cause }}), another exception occurred:
1172+
{% endif %}
1173+
{% endif %}
1174+
{% endifchanged %}
1175+
File "{{ frame.filename }}" in {{ frame.function }}
11271176
{% if frame.context_line %} {{ frame.lineno }}. {{ frame.context_line }}{% endif %}
11281177
{% endfor %}
11291178
{% if exception_type %}Exception Type: {{ exception_type }}{% if request %} at {{ request.path_info }}{% endif %}

docs/releases/1.9.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ Requests and Responses
172172
``status_code`` outside of the constructor will also modify the value of
173173
``reason_phrase``.
174174

175+
* The debug view now shows details of chained exceptions on Python 3.
176+
175177
Tests
176178
^^^^^
177179

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ doc_files = docs extras AUTHORS INSTALL LICENSE README.rst
33
install-script = scripts/rpm-install.sh
44

55
[flake8]
6-
exclude = build,.git,./django/utils/lru_cache.py,./django/utils/six.py,./django/conf/app_template/*,./django/dispatch/weakref_backports.py,./tests/.env,./xmlrunner
6+
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
77
ignore = E123,E128,E402,E501,W503,E731,W601
88
max-line-length = 119
99

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""
2+
Since this file contains Python 3 specific syntax, it's named without a test_
3+
prefix so the test runner won't try to import it. Instead, the test class is
4+
imported in test_debug.py, but only on Python 3.
5+
6+
This filename is also in setup.cfg flake8 exclude since the Python 2 syntax
7+
error (raise ... from ...) can't be silenced using NOQA.
8+
"""
9+
import sys
10+
11+
from django.test import RequestFactory, TestCase
12+
from django.views.debug import ExceptionReporter
13+
14+
15+
class Py3ExceptionReporterTests(TestCase):
16+
17+
rf = RequestFactory()
18+
19+
def test_reporting_of_nested_exceptions(self):
20+
request = self.rf.get('/test_view/')
21+
try:
22+
try:
23+
raise AttributeError('Top level')
24+
except AttributeError as explicit:
25+
try:
26+
raise ValueError('Second exception') from explicit
27+
except ValueError:
28+
raise IndexError('Final exception')
29+
except Exception:
30+
# Custom exception handler, just pass it into ExceptionReporter
31+
exc_type, exc_value, tb = sys.exc_info()
32+
33+
explicit_exc = 'The above exception ({0}) was the direct cause of the following exception:'
34+
implicit_exc = 'During handling of the above exception ({0}), another exception occurred:'
35+
reporter = ExceptionReporter(request, exc_type, exc_value, tb)
36+
html = reporter.get_traceback_html()
37+
self.assertIn(explicit_exc.format("Top level"), html)
38+
self.assertIn(implicit_exc.format("Second exception"), html)
39+
40+
text = reporter.get_traceback_text()
41+
self.assertIn(explicit_exc.format("Top level"), text)
42+
self.assertIn(implicit_exc.format("Second exception"), text)

tests/view_tests/tests/test_debug.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
sensitive_kwargs_function_caller, sensitive_method_view, sensitive_view,
2929
)
3030

31+
if six.PY3:
32+
from .py3_test_debug import Py3ExceptionReporterTests # NOQA
33+
3134

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

0 commit comments

Comments
 (0)