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:
Malcolm Tredinnick 2008-07-15 18:47:49 +00:00
parent 3b37c8151a
commit e7e4b8b0f7
1 changed files with 198 additions and 165 deletions

View File

@ -19,42 +19,6 @@ 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):
origin, (start, end) = exc_value.source
template_source = origin.reload()
context_lines = 10
line = 0
upto = 0
source_lines = []
before = during = after = ""
for num, next in enumerate(linebreak_iter(template_source)):
if start >= upto and end <= next:
line = num
before = escape(template_source[upto:start])
during = escape(template_source[start:end])
after = escape(template_source[end:next])
source_lines.append( (num, escape(template_source[upto:next])) )
upto = next
total = len(source_lines)
top = max(1, line - context_lines)
bottom = min(total, line + 1 + context_lines)
template_info = {
'message': exc_value.args[0],
'source_lines': source_lines[top:bottom],
'before': before,
'during': during,
'after': after,
'top': top,
'bottom': bottom,
'total': total,
'line': line,
'name': origin.name,
}
exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb)
return exc_info + (template_info,)
def get_safe_settings(): def get_safe_settings():
"Returns a dictionary of the settings module, with sensitive settings blurred out." "Returns a dictionary of the settings module, with sensitive settings blurred out."
settings_dict = {} settings_dict = {}
@ -71,102 +35,212 @@ def technical_500_response(request, exc_type, exc_value, tb):
Create a technical server error response. The last three arguments are Create a technical server error response. The last three arguments are
the values returned from sys.exc_info() and friends. the values returned from sys.exc_info() and friends.
""" """
html = get_traceback_html(request, exc_type, exc_value, tb) reporter = ExceptionReporter(request, exc_type, exc_value, tb)
html = reporter.get_traceback_html()
return HttpResponseServerError(html, mimetype='text/html') return HttpResponseServerError(html, mimetype='text/html')
def get_traceback_html(request, exc_type, exc_value, tb): class ExceptionReporter:
"Return HTML code for traceback." """
template_info = None A class to organize and coordinate reporting on exceptions.
template_does_not_exist = False """
loader_debug_info = None 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
# Handle deprecated string exceptions self.template_info = None
if isinstance(exc_type, basestring): self.template_does_not_exist = False
exc_value = Exception('Deprecated String Exception: %r' % exc_type) self.loader_debug_info = None
exc_type = type(exc_value)
if issubclass(exc_type, TemplateDoesNotExist): # Handle deprecated string exceptions
from django.template.loader import template_source_loaders if isinstance(self.exc_type, basestring):
template_does_not_exist = True self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type)
loader_debug_info = [] self.exc_type = type(self.exc_value)
for loader in template_source_loaders:
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()
context_lines = 10
line = 0
upto = 0
source_lines = []
before = during = after = ""
for num, next in enumerate(linebreak_iter(template_source)):
if start >= upto and end <= next:
line = num
before = escape(template_source[upto:start])
during = escape(template_source[start:end])
after = escape(template_source[end:next])
source_lines.append( (num, escape(template_source[upto:next])) )
upto = next
total = len(source_lines)
top = max(1, line - context_lines)
bottom = min(total, line + 1 + context_lines)
self.template_info = {
'message': self.exc_value.args[0],
'source_lines': source_lines[top:bottom],
'before': before,
'during': during,
'after': after,
'top': top,
'bottom': bottom,
'total': total,
'line': line,
'name': origin.name,
}
if hasattr(self.exc_value, 'exc_info') and self.exc_value.exc_info:
exc_type, exc_value, tb = self.exc_value.exc_info
def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
source = None
if loader is not None and hasattr(loader, "get_source"):
source = loader.get_source(module_name)
if source is not None:
source = source.splitlines()
if source is None:
try: try:
source_list_func = getattr(__import__(loader.__module__, {}, {}, ['get_template_sources']), 'get_template_sources') f = open(filename)
# NOTE: This assumes exc_value is the name of the template that try:
# the loader attempted to load. source = f.readlines()
template_list = [{'name': t, 'exists': os.path.exists(t)} \ finally:
for t in source_list_func(str(exc_value))] f.close()
except (ImportError, AttributeError): except (OSError, IOError):
template_list = [] pass
loader_debug_info.append({ if source is None:
'loader': loader.__module__ + '.' + loader.__name__, return None, [], None, []
'templates': template_list,
}) encoding = 'ascii'
if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'): for line in source[:2]:
exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) # File coding may be specified. Match pattern from PEP-263
frames = [] # (http://www.python.org/dev/peps/pep-0263/)
while tb is not None: match = re.search(r'coding[:=]\s*([-\w.]+)', line)
# support for __traceback_hide__ which is used by a few libraries if match:
# to hide internal frames. encoding = match.group(1)
if tb.tb_frame.f_locals.get('__traceback_hide__'): break
source = [unicode(sline, encoding, 'replace') for sline in source]
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = [line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]]
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 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: if not frames:
frames = [{ frames = [{
'filename': '&lt;unknown&gt;', 'filename': '&lt;unknown&gt;',
'function': '?', 'function': '?',
'lineno': '?', '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
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): def technical_404_response(request, exception):
"Create a technical 404 error response. The exception should be the Http404." "Create a technical 404 error response. The exception should be the Http404."
@ -199,47 +273,6 @@ def empty_urlconf(request):
}) })
return HttpResponse(t.render(c), mimetype='text/html') 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 (pre_context_lineno, pre_context, context_line, post_context).
"""
source = None
if loader is not None and hasattr(loader, "get_source"):
source = loader.get_source(module_name)
if source is not None:
source = source.splitlines()
if source is None:
try:
f = open(filename)
try:
source = f.readlines()
finally:
f.close()
except (OSError, IOError):
pass
if source is None:
return None, [], None, []
encoding = 'ascii'
for line in source[:2]:
# File coding may be specified. Match pattern from PEP-263
# (http://www.python.org/dev/peps/pep-0263/)
match = re.search(r'coding[:=]\s*([-\w.]+)', line)
if match:
encoding = match.group(1)
break
source = [unicode(sline, encoding, 'replace') for sline in source]
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = [line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]]
return lower_bound, pre_context, context_line, post_context
# #
# 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.