mirror of
				https://github.com/django/django.git
				synced 2025-10-26 07:06:08 +00:00 
			
		
		
		
	Fixed #6862 -- Refactored debug traceback extraction into an easy-to-use class.
Aside from being a little easier to read and use, this means you could subclass a request/response handler class (from django.core.handlers) to add your own traceback extraction handling in non-DEBUG environments and reuse this code. Thanks, Ned Batchelder. git-svn-id: http://code.djangoproject.com/svn/django/trunk@7927 bcc190cf-cafb-0310-a4f2-bffc1f526a37
This commit is contained in:
		| @@ -19,8 +19,101 @@ def linebreak_iter(template_source): | |||||||
|         p = template_source.find('\n', p+1) |         p = template_source.find('\n', p+1) | ||||||
|     yield len(template_source) + 1 |     yield len(template_source) + 1 | ||||||
|  |  | ||||||
| def get_template_exception_info(exc_type, exc_value, tb): | def get_safe_settings(): | ||||||
|     origin, (start, end) = exc_value.source |     "Returns a dictionary of the settings module, with sensitive settings blurred out." | ||||||
|  |     settings_dict = {} | ||||||
|  |     for k in dir(settings): | ||||||
|  |         if k.isupper(): | ||||||
|  |             if HIDDEN_SETTINGS.search(k): | ||||||
|  |                 settings_dict[k] = '********************' | ||||||
|  |             else: | ||||||
|  |                 settings_dict[k] = getattr(settings, k) | ||||||
|  |     return settings_dict | ||||||
|  |  | ||||||
|  | def technical_500_response(request, exc_type, exc_value, tb): | ||||||
|  |     """ | ||||||
|  |     Create a technical server error response. The last three arguments are | ||||||
|  |     the values returned from sys.exc_info() and friends. | ||||||
|  |     """ | ||||||
|  |     reporter = ExceptionReporter(request, exc_type, exc_value, tb) | ||||||
|  |     html = reporter.get_traceback_html() | ||||||
|  |     return HttpResponseServerError(html, mimetype='text/html') | ||||||
|  |  | ||||||
|  | class ExceptionReporter: | ||||||
|  |     """ | ||||||
|  |     A class to organize and coordinate reporting on exceptions. | ||||||
|  |     """ | ||||||
|  |     def __init__(self, request, exc_type, exc_value, tb): | ||||||
|  |         self.request = request | ||||||
|  |         self.exc_type = exc_type | ||||||
|  |         self.exc_value = exc_value | ||||||
|  |         self.tb = tb | ||||||
|  |  | ||||||
|  |         self.template_info = None | ||||||
|  |         self.template_does_not_exist = False | ||||||
|  |         self.loader_debug_info = None | ||||||
|  |  | ||||||
|  |         # Handle deprecated string exceptions | ||||||
|  |         if isinstance(self.exc_type, basestring): | ||||||
|  |             self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type) | ||||||
|  |             self.exc_type = type(self.exc_value) | ||||||
|  |  | ||||||
|  |     def get_traceback_html(self): | ||||||
|  |         "Return HTML code for traceback." | ||||||
|  |  | ||||||
|  |         if issubclass(self.exc_type, TemplateDoesNotExist): | ||||||
|  |             from django.template.loader import template_source_loaders | ||||||
|  |             self.template_does_not_exist = True | ||||||
|  |             self.loader_debug_info = [] | ||||||
|  |             for loader in template_source_loaders: | ||||||
|  |                 try: | ||||||
|  |                     source_list_func = getattr(__import__(loader.__module__, {}, {}, ['get_template_sources']), 'get_template_sources') | ||||||
|  |                     # NOTE: This assumes exc_value is the name of the template that | ||||||
|  |                     # the loader attempted to load. | ||||||
|  |                     template_list = [{'name': t, 'exists': os.path.exists(t)} \ | ||||||
|  |                         for t in source_list_func(str(self.exc_value))] | ||||||
|  |                 except (ImportError, AttributeError): | ||||||
|  |                     template_list = [] | ||||||
|  |                 self.loader_debug_info.append({ | ||||||
|  |                     'loader': loader.__module__ + '.' + loader.__name__, | ||||||
|  |                     'templates': template_list, | ||||||
|  |                 }) | ||||||
|  |         if settings.TEMPLATE_DEBUG and hasattr(self.exc_value, 'source'): | ||||||
|  |             self.get_template_exception_info() | ||||||
|  |  | ||||||
|  |         frames = self.get_traceback_frames() | ||||||
|  |  | ||||||
|  |         unicode_hint = '' | ||||||
|  |         if issubclass(self.exc_type, UnicodeError): | ||||||
|  |             start = getattr(self.exc_value, 'start', None) | ||||||
|  |             end = getattr(self.exc_value, 'end', None) | ||||||
|  |             if start is not None and end is not None: | ||||||
|  |                 unicode_str = self.exc_value.args[1] | ||||||
|  |                 unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace') | ||||||
|  |         from django import get_version | ||||||
|  |         t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') | ||||||
|  |         c = Context({ | ||||||
|  |             'exception_type': self.exc_type.__name__, | ||||||
|  |             'exception_value': smart_unicode(self.exc_value, errors='replace'), | ||||||
|  |             'unicode_hint': unicode_hint, | ||||||
|  |             'frames': frames, | ||||||
|  |             'lastframe': frames[-1], | ||||||
|  |             'request': self.request, | ||||||
|  |             'request_protocol': self.request.is_secure() and "https" or "http", | ||||||
|  |             'settings': get_safe_settings(), | ||||||
|  |             'sys_executable': sys.executable, | ||||||
|  |             'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], | ||||||
|  |             'server_time': datetime.datetime.now(), | ||||||
|  |             'django_version_info': get_version(), | ||||||
|  |             'sys_path' : sys.path, | ||||||
|  |             'template_info': self.template_info, | ||||||
|  |             'template_does_not_exist': self.template_does_not_exist, | ||||||
|  |             'loader_debug_info': self.loader_debug_info, | ||||||
|  |         }) | ||||||
|  |         return t.render(c) | ||||||
|  |  | ||||||
|  |     def get_template_exception_info(self): | ||||||
|  |         origin, (start, end) = self.exc_value.source | ||||||
|         template_source = origin.reload() |         template_source = origin.reload() | ||||||
|         context_lines = 10 |         context_lines = 10 | ||||||
|         line = 0 |         line = 0 | ||||||
| @@ -40,8 +133,8 @@ def get_template_exception_info(exc_type, exc_value, tb): | |||||||
|         top = max(1, line - context_lines) |         top = max(1, line - context_lines) | ||||||
|         bottom = min(total, line + 1 + context_lines) |         bottom = min(total, line + 1 + context_lines) | ||||||
|  |  | ||||||
|     template_info = { |         self.template_info = { | ||||||
|         'message': exc_value.args[0], |             'message': self.exc_value.args[0], | ||||||
|             'source_lines': source_lines[top:bottom], |             'source_lines': source_lines[top:bottom], | ||||||
|             'before': before, |             'before': before, | ||||||
|             'during': during, |             'during': during, | ||||||
| @@ -52,154 +145,10 @@ def get_template_exception_info(exc_type, exc_value, tb): | |||||||
|             'line': line, |             'line': line, | ||||||
|             'name': origin.name, |             'name': origin.name, | ||||||
|         } |         } | ||||||
|     exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb) |         if hasattr(self.exc_value, 'exc_info') and self.exc_value.exc_info: | ||||||
|     return exc_info + (template_info,) |             exc_type, exc_value, tb = self.exc_value.exc_info | ||||||
|  |  | ||||||
| def get_safe_settings(): |     def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None): | ||||||
|     "Returns a dictionary of the settings module, with sensitive settings blurred out." |  | ||||||
|     settings_dict = {} |  | ||||||
|     for k in dir(settings): |  | ||||||
|         if k.isupper(): |  | ||||||
|             if HIDDEN_SETTINGS.search(k): |  | ||||||
|                 settings_dict[k] = '********************' |  | ||||||
|             else: |  | ||||||
|                 settings_dict[k] = getattr(settings, k) |  | ||||||
|     return settings_dict |  | ||||||
|  |  | ||||||
| def technical_500_response(request, exc_type, exc_value, tb): |  | ||||||
|     """ |  | ||||||
|     Create a technical server error response. The last three arguments are |  | ||||||
|     the values returned from sys.exc_info() and friends. |  | ||||||
|     """ |  | ||||||
|     html = get_traceback_html(request, exc_type, exc_value, tb) |  | ||||||
|     return HttpResponseServerError(html, mimetype='text/html') |  | ||||||
|  |  | ||||||
| def get_traceback_html(request, exc_type, exc_value, tb): |  | ||||||
|     "Return HTML code for traceback." |  | ||||||
|     template_info = None |  | ||||||
|     template_does_not_exist = False |  | ||||||
|     loader_debug_info = None |  | ||||||
|  |  | ||||||
|     # Handle deprecated string exceptions |  | ||||||
|     if isinstance(exc_type, basestring): |  | ||||||
|         exc_value = Exception('Deprecated String Exception: %r' % exc_type) |  | ||||||
|         exc_type = type(exc_value) |  | ||||||
|  |  | ||||||
|     if issubclass(exc_type, TemplateDoesNotExist): |  | ||||||
|         from django.template.loader import template_source_loaders |  | ||||||
|         template_does_not_exist = True |  | ||||||
|         loader_debug_info = [] |  | ||||||
|         for loader in template_source_loaders: |  | ||||||
|             try: |  | ||||||
|                 source_list_func = getattr(__import__(loader.__module__, {}, {}, ['get_template_sources']), 'get_template_sources') |  | ||||||
|                 # NOTE: This assumes exc_value is the name of the template that |  | ||||||
|                 # the loader attempted to load. |  | ||||||
|                 template_list = [{'name': t, 'exists': os.path.exists(t)} \ |  | ||||||
|                     for t in source_list_func(str(exc_value))] |  | ||||||
|             except (ImportError, AttributeError): |  | ||||||
|                 template_list = [] |  | ||||||
|             loader_debug_info.append({ |  | ||||||
|                 'loader': loader.__module__ + '.' + loader.__name__, |  | ||||||
|                 'templates': template_list, |  | ||||||
|             }) |  | ||||||
|     if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'): |  | ||||||
|         exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) |  | ||||||
|     frames = [] |  | ||||||
|     while tb is not None: |  | ||||||
|         # support for __traceback_hide__ which is used by a few libraries |  | ||||||
|         # to hide internal frames. |  | ||||||
|         if tb.tb_frame.f_locals.get('__traceback_hide__'): |  | ||||||
|             tb = tb.tb_next |  | ||||||
|             continue |  | ||||||
|         filename = tb.tb_frame.f_code.co_filename |  | ||||||
|         function = tb.tb_frame.f_code.co_name |  | ||||||
|         lineno = tb.tb_lineno - 1 |  | ||||||
|         loader = tb.tb_frame.f_globals.get('__loader__') |  | ||||||
|         module_name = tb.tb_frame.f_globals.get('__name__') |  | ||||||
|         pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7, loader, module_name) |  | ||||||
|         if pre_context_lineno is not None: |  | ||||||
|             frames.append({ |  | ||||||
|                 'tb': tb, |  | ||||||
|                 'filename': filename, |  | ||||||
|                 'function': function, |  | ||||||
|                 'lineno': lineno + 1, |  | ||||||
|                 'vars': tb.tb_frame.f_locals.items(), |  | ||||||
|                 'id': id(tb), |  | ||||||
|                 'pre_context': pre_context, |  | ||||||
|                 'context_line': context_line, |  | ||||||
|                 'post_context': post_context, |  | ||||||
|                 'pre_context_lineno': pre_context_lineno + 1, |  | ||||||
|             }) |  | ||||||
|         tb = tb.tb_next |  | ||||||
|  |  | ||||||
|     if not frames: |  | ||||||
|         frames = [{ |  | ||||||
|             'filename': '<unknown>', |  | ||||||
|             'function': '?', |  | ||||||
|             'lineno': '?', |  | ||||||
|         }] |  | ||||||
|  |  | ||||||
|     unicode_hint = '' |  | ||||||
|     if issubclass(exc_type, UnicodeError): |  | ||||||
|         start = getattr(exc_value, 'start', None) |  | ||||||
|         end = getattr(exc_value, 'end', None) |  | ||||||
|         if start is not None and end is not None: |  | ||||||
|             unicode_str = exc_value.args[1] |  | ||||||
|             unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace') |  | ||||||
|     from django import get_version |  | ||||||
|     t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') |  | ||||||
|     c = Context({ |  | ||||||
|         'exception_type': exc_type.__name__, |  | ||||||
|         'exception_value': smart_unicode(exc_value, errors='replace'), |  | ||||||
|         'unicode_hint': unicode_hint, |  | ||||||
|         'frames': frames, |  | ||||||
|         'lastframe': frames[-1], |  | ||||||
|         'request': request, |  | ||||||
|         'request_protocol': request.is_secure() and "https" or "http", |  | ||||||
|         'settings': get_safe_settings(), |  | ||||||
|         'sys_executable': sys.executable, |  | ||||||
|         'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], |  | ||||||
|         'server_time': datetime.datetime.now(), |  | ||||||
|         'django_version_info': get_version(), |  | ||||||
|         'sys_path' : sys.path, |  | ||||||
|         'template_info': template_info, |  | ||||||
|         'template_does_not_exist': template_does_not_exist, |  | ||||||
|         'loader_debug_info': loader_debug_info, |  | ||||||
|     }) |  | ||||||
|     return t.render(c) |  | ||||||
|  |  | ||||||
| def technical_404_response(request, exception): |  | ||||||
|     "Create a technical 404 error response. The exception should be the Http404." |  | ||||||
|     try: |  | ||||||
|         tried = exception.args[0]['tried'] |  | ||||||
|     except (IndexError, TypeError): |  | ||||||
|         tried = [] |  | ||||||
|     else: |  | ||||||
|         if not tried: |  | ||||||
|             # tried exists but is an empty list. The URLconf must've been empty. |  | ||||||
|             return empty_urlconf(request) |  | ||||||
|  |  | ||||||
|     t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template') |  | ||||||
|     c = Context({ |  | ||||||
|         'root_urlconf': settings.ROOT_URLCONF, |  | ||||||
|         'request_path': request.path[1:], # Trim leading slash |  | ||||||
|         'urlpatterns': tried, |  | ||||||
|         'reason': str(exception), |  | ||||||
|         'request': request, |  | ||||||
|         'request_protocol': request.is_secure() and "https" or "http", |  | ||||||
|         'settings': get_safe_settings(), |  | ||||||
|     }) |  | ||||||
|     return HttpResponseNotFound(t.render(c), mimetype='text/html') |  | ||||||
|  |  | ||||||
| def empty_urlconf(request): |  | ||||||
|     "Create an empty URLconf 404 error response." |  | ||||||
|     t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf template') |  | ||||||
|     c = Context({ |  | ||||||
|         'project_name': settings.SETTINGS_MODULE.split('.')[0] |  | ||||||
|     }) |  | ||||||
|     return HttpResponse(t.render(c), mimetype='text/html') |  | ||||||
|  |  | ||||||
| def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None): |  | ||||||
|         """ |         """ | ||||||
|         Returns context_lines before and after lineno from file. |         Returns context_lines before and after lineno from file. | ||||||
|         Returns (pre_context_lineno, pre_context, context_line, post_context). |         Returns (pre_context_lineno, pre_context, context_line, post_context). | ||||||
| @@ -240,6 +189,90 @@ def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_na | |||||||
|  |  | ||||||
|         return lower_bound, pre_context, context_line, post_context |         return lower_bound, pre_context, context_line, post_context | ||||||
|  |  | ||||||
|  |     def get_traceback_frames(self): | ||||||
|  |         frames = [] | ||||||
|  |         tb = self.tb | ||||||
|  |         while tb is not None: | ||||||
|  |             # support for __traceback_hide__ which is used by a few libraries | ||||||
|  |             # to hide internal frames. | ||||||
|  |             if tb.tb_frame.f_locals.get('__traceback_hide__'): | ||||||
|  |                 tb = tb.tb_next | ||||||
|  |                 continue | ||||||
|  |             filename = tb.tb_frame.f_code.co_filename | ||||||
|  |             function = tb.tb_frame.f_code.co_name | ||||||
|  |             lineno = tb.tb_lineno - 1 | ||||||
|  |             loader = tb.tb_frame.f_globals.get('__loader__') | ||||||
|  |             module_name = tb.tb_frame.f_globals.get('__name__') | ||||||
|  |             pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name) | ||||||
|  |             if pre_context_lineno is not None: | ||||||
|  |                 frames.append({ | ||||||
|  |                     'tb': tb, | ||||||
|  |                     'filename': filename, | ||||||
|  |                     'function': function, | ||||||
|  |                     'lineno': lineno + 1, | ||||||
|  |                     'vars': tb.tb_frame.f_locals.items(), | ||||||
|  |                     'id': id(tb), | ||||||
|  |                     'pre_context': pre_context, | ||||||
|  |                     'context_line': context_line, | ||||||
|  |                     'post_context': post_context, | ||||||
|  |                     'pre_context_lineno': pre_context_lineno + 1, | ||||||
|  |                 }) | ||||||
|  |             tb = tb.tb_next | ||||||
|  |  | ||||||
|  |         if not frames: | ||||||
|  |             frames = [{ | ||||||
|  |                 'filename': '<unknown>', | ||||||
|  |                 'function': '?', | ||||||
|  |                 'lineno': '?', | ||||||
|  |                 'context_line': '???', | ||||||
|  |             }] | ||||||
|  |  | ||||||
|  |         return frames | ||||||
|  |  | ||||||
|  |     def format_exception(self): | ||||||
|  |         """ | ||||||
|  |         Return the same data as from traceback.format_exception. | ||||||
|  |         """ | ||||||
|  |         import traceback | ||||||
|  |         frames = self.get_traceback_frames() | ||||||
|  |         tb = [ (f['filename'], f['lineno'], f['function'], f['context_line']) for f in frames ] | ||||||
|  |         list = ['Traceback (most recent call last):\n'] | ||||||
|  |         list += traceback.format_list(tb) | ||||||
|  |         list += traceback.format_exception_only(self.exc_type, self.exc_value) | ||||||
|  |         return list | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def technical_404_response(request, exception): | ||||||
|  |     "Create a technical 404 error response. The exception should be the Http404." | ||||||
|  |     try: | ||||||
|  |         tried = exception.args[0]['tried'] | ||||||
|  |     except (IndexError, TypeError): | ||||||
|  |         tried = [] | ||||||
|  |     else: | ||||||
|  |         if not tried: | ||||||
|  |             # tried exists but is an empty list. The URLconf must've been empty. | ||||||
|  |             return empty_urlconf(request) | ||||||
|  |  | ||||||
|  |     t = Template(TECHNICAL_404_TEMPLATE, name='Technical 404 template') | ||||||
|  |     c = Context({ | ||||||
|  |         'root_urlconf': settings.ROOT_URLCONF, | ||||||
|  |         'request_path': request.path[1:], # Trim leading slash | ||||||
|  |         'urlpatterns': tried, | ||||||
|  |         'reason': str(exception), | ||||||
|  |         'request': request, | ||||||
|  |         'request_protocol': request.is_secure() and "https" or "http", | ||||||
|  |         'settings': get_safe_settings(), | ||||||
|  |     }) | ||||||
|  |     return HttpResponseNotFound(t.render(c), mimetype='text/html') | ||||||
|  |  | ||||||
|  | def empty_urlconf(request): | ||||||
|  |     "Create an empty URLconf 404 error response." | ||||||
|  |     t = Template(EMPTY_URLCONF_TEMPLATE, name='Empty URLConf template') | ||||||
|  |     c = Context({ | ||||||
|  |         'project_name': settings.SETTINGS_MODULE.split('.')[0] | ||||||
|  |     }) | ||||||
|  |     return HttpResponse(t.render(c), mimetype='text/html') | ||||||
|  |  | ||||||
| # | # | ||||||
| # Templates are embedded in the file so that we know the error handler will | # Templates are embedded in the file so that we know the error handler will | ||||||
| # always work even if the template loader is broken. | # always work even if the template loader is broken. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user