Django

Code

Changeset 7927

Show
Ignore:
Timestamp:
07/15/08 13:47:49 (4 months ago)
Author:
mtredinnick
Message:

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.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/trunk/django/views/debug.py

    r7863 r7927  
    1919        p = template_source.find('\n', p+1) 
    2020    yield len(template_source) + 1 
    21  
    22 def get_template_exception_info(exc_type, exc_value, tb): 
    23     origin, (start, end) = exc_value.source 
    24     template_source = origin.reload() 
    25     context_lines = 10 
    26     line = 0 
    27     upto = 0 
    28     source_lines = [] 
    29     before = during = after = "" 
    30     for num, next in enumerate(linebreak_iter(template_source)): 
    31         if start >= upto and end <= next: 
    32             line = num 
    33             before = escape(template_source[upto:start]) 
    34             during = escape(template_source[start:end]) 
    35             after = escape(template_source[end:next]) 
    36         source_lines.append( (num, escape(template_source[upto:next])) ) 
    37         upto = next 
    38     total = len(source_lines) 
    39  
    40     top = max(1, line - context_lines) 
    41     bottom = min(total, line + 1 + context_lines) 
    42  
    43     template_info = { 
    44         'message': exc_value.args[0], 
    45         'source_lines': source_lines[top:bottom], 
    46         'before': before, 
    47         'during': during, 
    48         'after': after, 
    49         'top': top, 
    50         'bottom': bottom, 
    51         'total': total, 
    52         'line': line, 
    53         'name': origin.name, 
    54     } 
    55     exc_info = hasattr(exc_value, 'exc_info') and exc_value.exc_info or (exc_type, exc_value, tb) 
    56     return exc_info + (template_info,) 
    5721 
    5822def get_safe_settings(): 
     
    7236    the values returned from sys.exc_info() and friends. 
    7337    """ 
    74     html = get_traceback_html(request, exc_type, exc_value, tb) 
     38    reporter = ExceptionReporter(request, exc_type, exc_value, tb) 
     39    html = reporter.get_traceback_html() 
    7540    return HttpResponseServerError(html, mimetype='text/html') 
    7641 
    77 def get_traceback_html(request, exc_type, exc_value, tb): 
    78     "Return HTML code for traceback." 
    79     template_info = None 
    80     template_does_not_exist = False 
    81     loader_debug_info = None 
    82  
    83     # Handle deprecated string exceptions 
    84     if isinstance(exc_type, basestring): 
    85         exc_value = Exception('Deprecated String Exception: %r' % exc_type) 
    86         exc_type = type(exc_value) 
    87  
    88     if issubclass(exc_type, TemplateDoesNotExist): 
    89         from django.template.loader import template_source_loaders 
    90         template_does_not_exist = True 
    91         loader_debug_info = [] 
    92         for loader in template_source_loaders: 
     42class ExceptionReporter: 
     43    """ 
     44    A class to organize and coordinate reporting on exceptions. 
     45    """ 
     46    def __init__(self, request, exc_type, exc_value, tb): 
     47        self.request = request 
     48        self.exc_type = exc_type 
     49        self.exc_value = exc_value 
     50        self.tb = tb 
     51 
     52        self.template_info = None 
     53        self.template_does_not_exist = False 
     54        self.loader_debug_info = None 
     55 
     56        # Handle deprecated string exceptions 
     57        if isinstance(self.exc_type, basestring): 
     58            self.exc_value = Exception('Deprecated String Exception: %r' % self.exc_type) 
     59            self.exc_type = type(self.exc_value) 
     60 
     61    def get_traceback_html(self): 
     62        "Return HTML code for traceback." 
     63 
     64        if issubclass(self.exc_type, TemplateDoesNotExist): 
     65            from django.template.loader import template_source_loaders 
     66            self.template_does_not_exist = True 
     67            self.loader_debug_info = [] 
     68            for loader in template_source_loaders: 
     69                try: 
     70                    source_list_func = getattr(__import__(loader.__module__, {}, {}, ['get_template_sources']), 'get_template_sources') 
     71                    # NOTE: This assumes exc_value is the name of the template that 
     72                    # the loader attempted to load. 
     73                    template_list = [{'name': t, 'exists': os.path.exists(t)} \ 
     74                        for t in source_list_func(str(self.exc_value))] 
     75                except (ImportError, AttributeError): 
     76                    template_list = [] 
     77                self.loader_debug_info.append({ 
     78                    'loader': loader.__module__ + '.' + loader.__name__, 
     79                    'templates': template_list, 
     80                }) 
     81        if settings.TEMPLATE_DEBUG and hasattr(self.exc_value, 'source'): 
     82            self.get_template_exception_info() 
     83 
     84        frames = self.get_traceback_frames() 
     85 
     86        unicode_hint = '' 
     87        if issubclass(self.exc_type, UnicodeError): 
     88            start = getattr(self.exc_value, 'start', None) 
     89            end = getattr(self.exc_value, 'end', None) 
     90            if start is not None and end is not None: 
     91                unicode_str = self.exc_value.args[1] 
     92                unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace') 
     93        from django import get_version 
     94        t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') 
     95        c = Context({ 
     96            'exception_type': self.exc_type.__name__, 
     97            'exception_value': smart_unicode(self.exc_value, errors='replace'), 
     98            'unicode_hint': unicode_hint, 
     99            'frames': frames, 
     100            'lastframe': frames[-1], 
     101            'request': self.request, 
     102            'request_protocol': self.request.is_secure() and "https" or "http", 
     103            'settings': get_safe_settings(), 
     104            'sys_executable': sys.executable, 
     105            'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], 
     106            'server_time': datetime.datetime.now(), 
     107            'django_version_info': get_version(), 
     108            'sys_path' : sys.path, 
     109            'template_info': self.template_info, 
     110            'template_does_not_exist': self.template_does_not_exist, 
     111            'loader_debug_info': self.loader_debug_info, 
     112        }) 
     113        return t.render(c) 
     114 
     115    def get_template_exception_info(self): 
     116        origin, (start, end) = self.exc_value.source 
     117        template_source = origin.reload() 
     118        context_lines = 10 
     119        line = 0 
     120        upto = 0 
     121        source_lines = [] 
     122        before = during = after = "" 
     123        for num, next in enumerate(linebreak_iter(template_source)): 
     124            if start >= upto and end <= next: 
     125                line = num 
     126                before = escape(template_source[upto:start]) 
     127                during = escape(template_source[start:end]) 
     128                after = escape(template_source[end:next]) 
     129            source_lines.append( (num, escape(template_source[upto:next])) ) 
     130            upto = next 
     131        total = len(source_lines) 
     132 
     133        top = max(1, line - context_lines) 
     134        bottom = min(total, line + 1 + context_lines) 
     135 
     136        self.template_info = { 
     137            'message': self.exc_value.args[0], 
     138            'source_lines': source_lines[top:bottom], 
     139            'before': before, 
     140            'during': during, 
     141            'after': after, 
     142            'top': top, 
     143            'bottom': bottom, 
     144            'total': total, 
     145            'line': line, 
     146            'name': origin.name, 
     147        } 
     148        if hasattr(self.exc_value, 'exc_info') and self.exc_value.exc_info: 
     149            exc_type, exc_value, tb = self.exc_value.exc_info 
     150 
     151    def _get_lines_from_file(self, filename, lineno, context_lines, loader=None, module_name=None): 
     152        """ 
     153        Returns context_lines before and after lineno from file. 
     154        Returns (pre_context_lineno, pre_context, context_line, post_context). 
     155        """ 
     156        source = None 
     157        if loader is not None and hasattr(loader, "get_source"): 
     158            source = loader.get_source(module_name) 
     159            if source is not None: 
     160                source = source.splitlines() 
     161        if source is None: 
    93162            try: 
    94                 source_list_func = getattr(__import__(loader.__module__, {}, {}, ['get_template_sources']), 'get_template_sources') 
    95                 # NOTE: This assumes exc_value is the name of the template that 
    96                 # the loader attempted to load. 
    97                 template_list = [{'name': t, 'exists': os.path.exists(t)} \ 
    98                     for t in source_list_func(str(exc_value))] 
    99             except (ImportError, AttributeError): 
    100                 template_list = [] 
    101             loader_debug_info.append({ 
    102                 'loader': loader.__module__ + '.' + loader.__name__, 
    103                 'templates': template_list, 
    104             }) 
    105     if settings.TEMPLATE_DEBUG and hasattr(exc_value, 'source'): 
    106         exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) 
    107     frames = [] 
    108     while tb is not None: 
    109         # support for __traceback_hide__ which is used by a few libraries 
    110         # to hide internal frames. 
    111         if tb.tb_frame.f_locals.get('__traceback_hide__'): 
     163                f = open(filename) 
     164                try: 
     165                    source = f.readlines() 
     166                finally: 
     167                    f.close() 
     168            except (OSError, IOError): 
     169                pass 
     170        if source is None: 
     171            return None, [], None, [] 
     172 
     173        encoding = 'ascii' 
     174        for line in source[:2]: 
     175            # File coding may be specified. Match pattern from PEP-263 
     176            # (http://www.python.org/dev/peps/pep-0263/) 
     177            match = re.search(r'coding[:=]\s*([-\w.]+)', line) 
     178            if match: 
     179                encoding = match.group(1) 
     180                break 
     181        source = [unicode(sline, encoding, 'replace') for sline in source] 
     182 
     183        lower_bound = max(0, lineno - context_lines) 
     184        upper_bound = lineno + context_lines 
     185 
     186        pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] 
     187        context_line = source[lineno].strip('\n') 
     188        post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] 
     189 
     190        return lower_bound, pre_context, context_line, post_context 
     191 
     192    def get_traceback_frames(self): 
     193        frames = [] 
     194        tb = self.tb 
     195        while tb is not None: 
     196            # support for __traceback_hide__ which is used by a few libraries 
     197            # to hide internal frames. 
     198            if tb.tb_frame.f_locals.get('__traceback_hide__'): 
     199                tb = tb.tb_next 
     200                continue 
     201            filename = tb.tb_frame.f_code.co_filename 
     202            function = tb.tb_frame.f_code.co_name 
     203            lineno = tb.tb_lineno - 1 
     204            loader = tb.tb_frame.f_globals.get('__loader__') 
     205            module_name = tb.tb_frame.f_globals.get('__name__') 
     206            pre_context_lineno, pre_context, context_line, post_context = self._get_lines_from_file(filename, lineno, 7, loader, module_name) 
     207            if pre_context_lineno is not None: 
     208                frames.append({ 
     209                    'tb': tb, 
     210                    'filename': filename, 
     211                    'function': function, 
     212                    'lineno': lineno + 1, 
     213                    'vars': tb.tb_frame.f_locals.items(), 
     214                    'id': id(tb), 
     215                    'pre_context': pre_context, 
     216                    'context_line': context_line, 
     217                    'post_context': post_context, 
     218                    'pre_context_lineno': pre_context_lineno + 1, 
     219                }) 
    112220            tb = tb.tb_next 
    113             continue 
    114         filename = tb.tb_frame.f_code.co_filename 
    115         function = tb.tb_frame.f_code.co_name 
    116         lineno = tb.tb_lineno - 1 
    117         loader = tb.tb_frame.f_globals.get('__loader__') 
    118         module_name = tb.tb_frame.f_globals.get('__name__') 
    119         pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7, loader, module_name) 
    120         if pre_context_lineno is not None: 
    121             frames.append({ 
    122                 'tb': tb, 
    123                 'filename': filename, 
    124                 'function': function, 
    125                 'lineno': lineno + 1, 
    126                 'vars': tb.tb_frame.f_locals.items(), 
    127                 'id': id(tb), 
    128                 'pre_context': pre_context, 
    129                 'context_line': context_line, 
    130                 'post_context': post_context, 
    131                 'pre_context_lineno': pre_context_lineno + 1, 
    132             }) 
    133         tb = tb.tb_next 
    134  
    135     if not frames: 
    136         frames = [{ 
    137             'filename': '&lt;unknown&gt;', 
    138             'function': '?', 
    139             'lineno': '?', 
    140         }] 
    141  
    142     unicode_hint = '' 
    143     if issubclass(exc_type, UnicodeError): 
    144         start = getattr(exc_value, 'start', None) 
    145         end = getattr(exc_value, 'end', None) 
    146         if start is not None and end is not None: 
    147             unicode_str = exc_value.args[1] 
    148             unicode_hint = smart_unicode(unicode_str[max(start-5, 0):min(end+5, len(unicode_str))], 'ascii', errors='replace') 
    149     from django import get_version 
    150     t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') 
    151     c = Context({ 
    152         'exception_type': exc_type.__name__, 
    153         'exception_value': smart_unicode(exc_value, errors='replace'), 
    154         'unicode_hint': unicode_hint, 
    155         'frames': frames, 
    156         'lastframe': frames[-1], 
    157         'request': request, 
    158         'request_protocol': request.is_secure() and "https" or "http", 
    159         'settings': get_safe_settings(), 
    160         'sys_executable': sys.executable, 
    161         'sys_version_info': '%d.%d.%d' % sys.version_info[0:3], 
    162         'server_time': datetime.datetime.now(), 
    163         'django_version_info': get_version(), 
    164         'sys_path' : sys.path, 
    165         'template_info': template_info, 
    166         'template_does_not_exist': template_does_not_exist, 
    167         'loader_debug_info': loader_debug_info, 
    168     }) 
    169     return t.render(c) 
     221 
     222        if not frames: 
     223            frames = [{ 
     224                'filename': '&lt;unknown&gt;', 
     225                'function': '?', 
     226                'lineno': '?', 
     227                'context_line': '???', 
     228            }] 
     229 
     230        return frames 
     231 
     232    def format_exception(self): 
     233        """ 
     234        Return the same data as from traceback.format_exception. 
     235        """ 
     236        import traceback 
     237        frames = self.get_traceback_frames() 
     238        tb = [ (f['filename'], f['lineno'], f['function'], f['context_line']) for f in frames ] 
     239        list = ['Traceback (most recent call last):\n'] 
     240        list += traceback.format_list(tb) 
     241        list += traceback.format_exception_only(self.exc_type, self.exc_value) 
     242        return list 
     243 
    170244 
    171245def technical_404_response(request, exception): 
     
    199273    }) 
    200274    return HttpResponse(t.render(c), mimetype='text/html') 
    201  
    202 def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None): 
    203     """ 
    204     Returns context_lines before and after lineno from file. 
    205     Returns (pre_context_lineno, pre_context, context_line, post_context). 
    206     """ 
    207     source = None 
    208     if loader is not None and hasattr(loader, "get_source"): 
    209         source = loader.get_source(module_name) 
    210         if source is not None: 
    211             source = source.splitlines() 
    212     if source is None: 
    213         try: 
    214             f = open(filename) 
    215             try: 
    216                 source = f.readlines() 
    217             finally: 
    218                 f.close() 
    219         except (OSError, IOError): 
    220             pass 
    221     if source is None: 
    222         return None, [], None, [] 
    223  
    224     encoding = 'ascii' 
    225     for line in source[:2]: 
    226         # File coding may be specified. Match pattern from PEP-263 
    227         # (http://www.python.org/dev/peps/pep-0263/) 
    228         match = re.search(r'coding[:=]\s*([-\w.]+)', line) 
    229         if match: 
    230             encoding = match.group(1) 
    231             break 
    232     source = [unicode(sline, encoding, 'replace') for sline in source] 
    233  
    234     lower_bound = max(0, lineno - context_lines) 
    235     upper_bound = lineno + context_lines 
    236  
    237     pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] 
    238     context_line = source[lineno].strip('\n') 
    239     post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] 
    240  
    241     return lower_bound, pre_context, context_line, post_context 
    242275 
    243276#