mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +00:00 
			
		
		
		
	Fixes #23643 -- Added chained exception details to debug view.
This commit is contained in:
		
				
					committed by
					
						 Tim Graham
						Tim Graham
					
				
			
			
				
	
			
			
			
						parent
						
							ae87ad005f
						
					
				
				
					commit
					8414fcf16b
				
			| @@ -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 %} | ||||
|   | ||||
| @@ -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 | ||||
| ^^^^^ | ||||
|  | ||||
|   | ||||
| @@ -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
									
								
								tests/view_tests/tests/py3_test_debug.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								tests/view_tests/tests/py3_test_debug.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user