Commit e7e4b8b0 authored by Malcolm Tredinnick's avatar Malcolm Tredinnick
Browse files

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
parent 3b37c815
Loading
Loading
Loading
Loading
+202 −169
Original line number Diff line number Diff line
@@ -19,8 +19,101 @@ def linebreak_iter(template_source):
        p = template_source.find('\n', p+1)
    yield len(template_source) + 1

def get_template_exception_info(exc_type, exc_value, tb):
    origin, (start, end) = exc_value.source
def get_safe_settings():
    "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()
        context_lines = 10
        line = 0
@@ -40,8 +133,8 @@ def get_template_exception_info(exc_type, exc_value, tb):
        top = max(1, line - context_lines)
        bottom = min(total, line + 1 + context_lines)

    template_info = {
        'message': exc_value.args[0],
        self.template_info = {
            'message': self.exc_value.args[0],
            'source_lines': source_lines[top:bottom],
            'before': before,
            'during': during,
@@ -52,59 +145,53 @@ def get_template_exception_info(exc_type, exc_value, tb):
            '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,)
        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_safe_settings():
    "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):
    def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None):
        """
    Create a technical server error response. The last three arguments are
    the values returned from sys.exc_info() and friends.
        Returns context_lines before and after lineno from file.
        Returns (pre_context_lineno, pre_context, context_line, post_context).
        """
    html = get_traceback_html(request, exc_type, exc_value, tb)
    return HttpResponseServerError(html, mimetype='text/html')
        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, []

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
        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]

    # Handle deprecated string exceptions
    if isinstance(exc_type, basestring):
        exc_value = Exception('Deprecated String Exception: %r' % exc_type)
        exc_type = type(exc_value)
        lower_bound = max(0, lineno - context_lines)
        upper_bound = lineno + context_lines

    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)
        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.
@@ -116,7 +203,7 @@ def get_traceback_html(request, exc_type, exc_value, tb):
            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)
            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,
@@ -137,36 +224,23 @@ def get_traceback_html(request, exc_type, exc_value, tb):
                'filename': '<unknown>',
                'function': '?',
                'lineno': '?',
                'context_line': '???',
            }]

    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)
        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."
@@ -199,47 +273,6 @@ def empty_urlconf(request):
    })
    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
# always work even if the template loader is broken.