Django

Code

Ticket #3527: django_debugger.patch

File django_debugger.patch, 17.0 kB (added by Marek Kubica <pythonmailing@web.de>, 2 years ago)

Patch by Armin Ronacher to add a debugger to Django's exceptions

  • django/conf/global_settings.py

    old new  
    1111 
    1212DEBUG = False 
    1313TEMPLATE_DEBUG = False 
     14DEBUGGER_ENABLED = False 
    1415 
    1516# Whether to use the "Etag" header. This saves bandwidth but slows down performance. 
    1617USE_ETAGS = False 
  • django/views/debug.py

    old new  
    22from django.template import Template, Context, TemplateDoesNotExist 
    33from django.utils.html import escape 
    44from django.http import HttpResponseServerError, HttpResponseNotFound 
    5 import os, re 
     5import sys, os, re, code, threading 
    66 
    77HIDDEN_SETTINGS = re.compile('SECRET|PASSWORD|PROFANITIES_LIST') 
    88 
     9def new_debugger_id(): 
     10    import time 
     11    import md5 
     12    return md5.new(str(time.time())).hexdigest() 
     13 
    914def linebreak_iter(template_source): 
    1015    yield 0 
    1116    p = template_source.find('\n') 
     
    6671    Create a technical server error response. The last three arguments are 
    6772    the values returned from sys.exc_info() and friends. 
    6873    """ 
     74    if settings.DEBUGGER_ENABLED: 
     75        debugger_id = new_debugger_id() 
     76        debugger_enabled = True 
     77        frame_storage = console.debug_sessions[debugger_id] = {} 
     78    else: 
     79        debugger_id = None 
     80        debugger_enabled = False 
    6981    template_info = None 
    7082    template_does_not_exist = False 
    7183    loader_debug_info = None 
     
    90102        exc_type, exc_value, tb, template_info = get_template_exception_info(exc_type, exc_value, tb) 
    91103    frames = [] 
    92104    while tb is not None: 
     105        # support for __traceback_hide__ which is used by a few libraries 
     106        # to hide internal frames. 
     107        if tb.tb_frame.f_locals.get('__traceback_hide__'): 
     108            tb = tb.tb_next 
     109            continue 
     110        if debugger_enabled: 
     111            frame_storage[str(id(tb))] = tb.tb_frame 
    93112        filename = tb.tb_frame.f_code.co_filename 
    94113        function = tb.tb_frame.f_code.co_name 
    95114        lineno = tb.tb_lineno - 1 
    96         pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7) 
    97         if pre_context_lineno: 
     115        loader = tb.tb_frame.f_globals.get('__loader__') 
     116        module_name = tb.tb_frame.f_globals.get('__name__') 
     117        pre_context_lineno, pre_context, context_line, post_context = _get_lines_from_file(filename, lineno, 7, loader, module_name) 
     118        if pre_context_lineno is not None: 
    98119            frames.append({ 
    99120                'tb': tb, 
    100121                'filename': filename, 
     
    117138        }] 
    118139    t = Template(TECHNICAL_500_TEMPLATE, name='Technical 500 template') 
    119140    c = Context({ 
     141        'debugger_id': debugger_id, 
     142        'debugger_enabled': debugger_enabled, 
    120143        'exception_type': exc_type.__name__, 
    121144        'exception_value': exc_value, 
    122145        'frames': frames, 
     
    161184    }) 
    162185    return HttpResponseNotFound(t.render(c), mimetype='text/html') 
    163186 
    164 def _get_lines_from_file(filename, lineno, context_lines): 
     187def _get_lines_from_file(filename, lineno, context_lines, loader=None, module_name=None): 
    165188    """ 
    166189    Returns context_lines before and after lineno from file. 
    167190    Returns (pre_context_lineno, pre_context, context_line, post_context). 
    168191    """ 
    169     try: 
    170         source = open(filename).readlines() 
    171         lower_bound = max(0, lineno - context_lines) 
    172         upper_bound = lineno + context_lines 
     192    source = None 
     193    if loader is not None: 
     194        source = loader.get_source(module_name).splitlines() 
     195    else: 
     196        try: 
     197            f = open(filename) 
     198            try: 
     199                source = f.readlines() 
     200            finally: 
     201                f.close() 
     202        except (OSError, IOError): 
     203            pass 
     204    if source is None: 
     205        return None, [], None, [] 
    173206 
    174         pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] 
    175         context_line = source[lineno].strip('\n') 
    176         post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]] 
     207    lower_bound = max(0, lineno - context_lines) 
     208    upper_bound = lineno + context_lines 
    177209 
    178         return lower_bound, pre_context, context_line, post_context 
    179     except (OSError, IOError): 
    180         return None, [], None, [
     210    pre_context = [line.strip('\n') for line in source[lower_bound:lineno]] 
     211    context_line = source[lineno].strip('\n') 
     212    post_context = [line.strip('\n') for line in source[lineno+1:upper_bound]
    181213 
     214    return lower_bound, pre_context, context_line, post_context 
     215 
     216 
     217class DebugCapture(object): 
     218    """ 
     219    Class that wraps sys.stdout in order to get the output 
     220    for the debugger. threadsafe but quite slow. 
     221    """ 
     222    _orig = None 
     223 
     224    def __init__(self): 
     225        self._buffer = {} 
     226 
     227    def install(cls): 
     228        if cls._orig: 
     229            return 
     230        cls._orig = sys.stdout 
     231        sys.stdout = cls() 
     232    install = classmethod(install) 
     233 
     234    def push(self): 
     235        from cStringIO import StringIO 
     236        tid = threading.currentThread() 
     237        self._buffer[tid] = StringIO() 
     238 
     239    def release(self): 
     240        tid = threading.currentThread() 
     241        if tid in self._buffer: 
     242            result = self._buffer[tid].getvalue() 
     243            del self._buffer[tid] 
     244        else: 
     245            result = '' 
     246        return result 
     247 
     248    def write(self, d): 
     249        tid = threading.currentThread() 
     250        if tid in self._buffer: 
     251            self._buffer[tid].write(d) 
     252        else: 
     253            self._orig.write(d) 
     254 
     255 
     256class PlainDebugger(code.InteractiveInterpreter): 
     257    """ 
     258    Subclass of the python interactive interpreter that 
     259    automatically captures stdout and buffers older input. 
     260    """ 
     261 
     262    def __init__(self, locals=None, globals=None): 
     263        self.globals = globals 
     264        code.InteractiveInterpreter.__init__(self, locals) 
     265        self.prompt = '>>> ' 
     266        self.buffer = [] 
     267 
     268    def runsource(self, source): 
     269        # installs the debug capture on first access 
     270        DebugCapture.install() 
     271        prompt = self.prompt 
     272        sys.stdout.push() 
     273        try: 
     274            source_to_eval = ''.join(self.buffer + [source]) 
     275            if code.InteractiveInterpreter.runsource(self, 
     276               source_to_eval, '<debugger>', 'single'): 
     277                self.prompt = '... ' 
     278                self.buffer.append(source) 
     279            else: 
     280                self.prompt = '>>> ' 
     281                del self.buffer[:] 
     282        finally: 
     283            return prompt + source + sys.stdout.release() 
     284 
     285    def runcode(self, code): 
     286        try: 
     287            exec code in self.globals, self.locals 
     288        except: 
     289            self.showtraceback() 
     290 
     291    def write(self, data): 
     292        sys.stdout.write(data) 
     293 
     294 
     295class AjaxDebugger(object): 
     296    """ 
     297    The AJAX Debugger 
     298    """ 
     299 
     300    def __init__(self): 
     301        self.debug_sessions = {} 
     302        self.consoles = {} 
     303 
     304    def send(self, debugger, frame, cmd): 
     305        if debugger not in self.debug_sessions: 
     306            return '!!! expired debugger !!!' 
     307        session = self.debug_sessions[debugger] 
     308        if frame not in session: 
     309            return '!!! unknown frame !!!' 
     310        key = '%s|%s' % (debugger, frame) 
     311        if key not in self.consoles: 
     312            self.consoles[key] = PlainDebugger( 
     313                session[frame].f_globals, 
     314                session[frame].f_locals 
     315            ) 
     316        return self.consoles[key].runsource(cmd) 
     317 
     318 
     319console = AjaxDebugger() 
     320 
    182321# 
    183322# Templates are embedded in the file so that we know the error handler will 
    184323# always work even if the template loader is broken. 
    185324# 
    186325 
    187 TECHNICAL_500_TEMPLATE = """ 
     326TECHNICAL_500_TEMPLATE = r""" 
    188327<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> 
    189328<html lang="en"> 
    190329<head> 
     
    197336    body * * { padding:0; } 
    198337    body { font:small sans-serif; } 
    199338    body>div { border-bottom:1px solid #ddd; } 
     339    a { color: #333; } 
    200340    h1 { font-weight:normal; } 
    201341    h2 { margin-bottom:.8em; } 
    202342    h2 span { font-size:80%; color:#666; font-weight:normal; } 
     
    212352    table td.code div { overflow:hidden; } 
    213353    table.source th { color:#666; } 
    214354    table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; } 
     355    form.debug { display: block; padding: 10px 20px 10px 40px; } 
     356    form.debug input { margin-top: 5px; width: 100%; } 
    215357    ul.traceback { list-style-type:none; } 
    216358    ul.traceback li.frame { margin-bottom:1em; } 
    217359    div.context { margin: 10px 0; } 
     
    219361    div.context ol li { font-family:monospace; white-space:pre; color:#666; cursor:pointer; } 
    220362    div.context ol.context-line li { color:black; background-color:#ccc; } 
    221363    div.context ol.context-line li span { float: right; } 
    222     div.commands { margin-left: 40px; } 
    223     div.commands a { color:black; text-decoration:none; } 
     364    ul.commands { margin-left: 40px; padding: 0; list-style: none; } 
     365    ul.commands a { color:black; text-decoration:none; } 
    224366    #summary { background: #ffc; } 
    225367    #summary h2 { font-weight: normal; color: #666; } 
    226368    #explanation { background:#eee; } 
     
    236378  </style> 
    237379  <script type="text/javascript"> 
    238380  //<!-- 
    239     function getElementsByClassName(oElm, strTagName, strClassName){ 
    240         // Written by Jonathan Snook, http://www.snook.ca/jon; Add-ons by Robert Nyman, http://www.robertnyman.com 
    241         var arrElements = (strTagName == "*" && document.all)? document.all : 
     381    var DEBUG_ID = {% if debugger_enabled %}'{{ debugger_id }}'{% else %}null{% endif %}; 
     382 
     383    function getElementsByClassName(oElm, strTagName, strClassName) { 
     384      // Written by Jonathan Snook, http://www.snook.ca/jon; Add-ons by Robert Nyman, http://www.robertnyman.com 
     385      var arrElements = (strTagName == "*" && document.all) ? document.all : 
    242386        oElm.getElementsByTagName(strTagName); 
    243         var arrReturnElements = new Array(); 
    244         strClassName = strClassName.replace(/\-/g, "\\-"); 
    245         var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)"); 
    246         var oElement; 
    247         for(var i=0; i<arrElements.length; i++){ 
    248             oElement = arrElements[i]; 
    249             if(oRegExp.test(oElement.className)){ 
    250                 arrReturnElements.push(oElement); 
    251             } 
     387      var arrReturnElements = new Array(); 
     388      strClassName = strClassName.replace(/\-/g, "\\-"); 
     389      var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)"); 
     390      var oElement; 
     391      for(var i=0; i<arrElements.length; i++) { 
     392        oElement = arrElements[i]; 
     393        if(oRegExp.test(oElement.className)) { 
     394          arrReturnElements.push(oElement); 
    252395        } 
    253         return (arrReturnElements) 
     396      } 
     397      return arrReturnElements; 
    254398    } 
    255399    function hideAll(elems) { 
    256400      for (var e = 0; e < elems.length; e++) { 
     
    259403    } 
    260404    window.onload = function() { 
    261405      hideAll(getElementsByClassName(document, 'table', 'vars')); 
     406      hideAll(getElementsByClassName(document, 'form', 'debug')); 
    262407      hideAll(getElementsByClassName(document, 'ol', 'pre-context')); 
    263408      hideAll(getElementsByClassName(document, 'ol', 'post-context')); 
    264409      hideAll(getElementsByClassName(document, 'div', 'pastebin')); 
     
    280425      s.innerHTML = s.innerHTML == uarr ? darr : uarr; 
    281426      return false; 
    282427    } 
     428    function debugToggle(link, id) { 
     429      toggle('d' + id); 
     430      var s = link.getElementsByTagName('span')[0]; 
     431      var uarr = String.fromCharCode(0x25b6); 
     432      var darr = String.fromCharCode(0x25bc); 
     433      s.innerHTML = s.innerHTML == uarr ? darr : uarr; 
     434      return false; 
     435    } 
    283436    function switchPastebinFriendly(link) { 
    284437      s1 = "Switch to copy-and-paste view"; 
    285438      s2 = "Switch back to interactive view"; 
     
    287440      toggle('browserTraceback', 'pastebinTraceback'); 
    288441      return false; 
    289442    } 
     443    function sendDebugCommand(frameID, command, callback) { 
     444      var activex = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0']; 
     445      var con = null; 
     446      try { 
     447        con = new XMLHttpRequest(); 
     448      } 
     449      catch (e) { 
     450        for (var i=0; i < activex.length; i++) { 
     451          try { 
     452            con = new ActiveXObject(activex[i]); 
     453          } 
     454          catch (e) {}; 
     455          if (con) { 
     456            break; 
     457          } 
     458        } 
     459      } 
     460      var data = 'tb=' + DEBUG_ID + '&' + 
     461                 'frame=' + frameID + '&' + 
     462                 'cmd=' + encodeURIComponent(command); 
     463      con.onreadystatechange = function() { 
     464        if (con.readyState == 4) { 
     465          callback(con.responseText); 
     466        } 
     467      }; 
     468      con.open('POST', '?__send_to_django_debugger__=yes'); 
     469      con.send(data); 
     470    } 
     471    function submitCommand(frameID) { 
     472      var input = document.getElementById('di' + frameID); 
     473      var output = document.getElementById('do' + frameID); 
     474      output = (output.firstChild || output.appendChild(document.createTextNode(''))); 
     475      if (input.value == 'clear') { 
     476        output.nodeValue = ''; 
     477      } 
     478      else { 
     479        sendDebugCommand(frameID, input.value + '\n', function(value) { 
     480          output.nodeValue += value; 
     481        }); 
     482      } 
     483      input.value = ''; 
     484      input.focus(); 
     485      return false; 
     486    } 
    290487    //--> 
    291488  </script> 
    292489</head> 
     
    314511    </tr> 
    315512    <tr> 
    316513      <th>Exception Location:</th> 
    317       <td>{{ lastframe.filename }} in {{ lastframe.function }}, line {{ lastframe.lineno }}</td> 
     514      <td>{{ lastframe.filename|escape }} in {{ lastframe.function|escape }}, line {{ lastframe.lineno }}</td> 
    318515    </tr> 
    319516  </table> 
    320517</div> 
     
    361558    <ul class="traceback"> 
    362559      {% for frame in frames %} 
    363560        <li class="frame"> 
    364           <code>{{ frame.filename }}</code> in <code>{{ frame.function }}</code> 
     561          <code>{{ frame.filename|escape }}</code> in <code>{{ frame.function|escape }}</code> 
    365562 
    366563          {% if frame.context_line %} 
    367564            <div class="context" id="c{{ frame.id }}"> 
     
    375572            </div> 
    376573          {% endif %} 
    377574 
    378           {% if frame.vars %} 
    379             <div class="commands"> 
    380                 <a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a> 
    381             </div> 
    382             <table class="vars" id="v{{ frame.id }}"> 
    383               <thead> 
    384                 <tr> 
    385                   <th>Variable</th> 
    386                   <th>Value</th> 
    387                 </tr> 
    388               </thead> 
    389               <tbody> 
    390                 {% for var in frame.vars|dictsort:"0" %} 
     575          {% if frame.vars or debugger_enabled %} 
     576            <ul class="commands"> 
     577              {% if frame.vars %} 
     578                <li><a href="#" onclick="return varToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Local vars</a></li> 
     579              {% endif %} 
     580              {% if debugger_enabled %} 
     581                <li><a href="#" onclick="return debugToggle(this, '{{ frame.id }}')"><span>&#x25b6;</span> Debug</a></li> 
     582              {% endif %} 
     583            </ul> 
     584            {% if frame.vars %} 
     585              <table class="vars" id="v{{ frame.id }}"> 
     586                <thead> 
    391587                  <tr> 
    392                     <td>{{ var.0 }}</td
    393                     <td class="code"><div>{{ var.1|pprint|escape }}</div></td
     588                    <th>Variable</th
     589                    <th>Value</th
    394590                  </tr> 
    395                 {% endfor %} 
    396               </tbody> 
    397             </table> 
     591                </thead> 
     592                <tbody> 
     593                  {% for var in frame.vars|dictsort:"0" %} 
     594                    <tr> 
     595                      <td>{{ var.0 }}</td> 
     596                      <td class="code"><div>{{ var.1|pprint|escape }}</div></td> 
     597                    </tr> 
     598                  {% endfor %} 
     599                </tbody> 
     600              </table> 
     601            {% endif %} 
     602            {% if debugger_enabled %} 
     603              <form class="debug" id="d{{ frame.id }}" onsubmit="return submitCommand('{{ frame.id }}')"> 
     604                <pre id="do{{ frame.id }}"></pre> 
     605                <input type="text" id="di{{ frame.id }}"> 
     606              </form> 
     607            {% endif %} 
    398608          {% endif %} 
    399609        </li> 
    400610      {% endfor %} 
  • django/middleware/common.py

    old new  
    2424        settings.APPEND_SLASH and settings.PREPEND_WWW 
    2525        """ 
    2626 
     27        # Handle Debugger AJAX Requests 
     28        if settings.DEBUGGER_ENABLED and \ 
     29           request.GET.get('__send_to_django_debugger__') == 'yes': 
     30            from django.views.debug import console 
     31            rv = console.send( 
     32                debugger=request.POST.get('tb'), 
     33                frame=request.POST.get('frame'), 
     34                cmd=request.POST.get('cmd', '') 
     35            ) 
     36            return http.HttpResponse(rv, mimetype='text/plain') 
     37 
    2738        # Check for denied User-Agents 
    2839        if request.META.has_key('HTTP_USER_AGENT'): 
    2940            for user_agent_regex in settings.DISALLOWED_USER_AGENTS: