Home | History | Annotate | Download | only in debug
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 """
      4 Middleware that displays everything that is printed inline in
      5 application pages.
      6 
      7 Anything printed during the request will get captured and included on
      8 the page.  It will usually be included as a floating element in the
      9 top right hand corner of the page.  If you want to override this
     10 you can include a tag in your template where it will be placed::
     11 
     12   <pre id="paste-debug-prints"></pre>
     13 
     14 You might want to include ``style="white-space: normal"``, as all the
     15 whitespace will be quoted, and this allows the text to wrap if
     16 necessary.
     17 
     18 """
     19 
     20 from cStringIO import StringIO
     21 import re
     22 import cgi
     23 from paste.util import threadedprint
     24 from paste import wsgilib
     25 from paste import response
     26 import six
     27 import sys
     28 
     29 _threadedprint_installed = False
     30 
     31 __all__ = ['PrintDebugMiddleware']
     32 
     33 class TeeFile(object):
     34 
     35     def __init__(self, files):
     36         self.files = files
     37 
     38     def write(self, v):
     39         if isinstance(v, unicode):
     40             # WSGI is picky in this case
     41             v = str(v)
     42         for file in self.files:
     43             file.write(v)
     44 
     45 class PrintDebugMiddleware(object):
     46 
     47     """
     48     This middleware captures all the printed statements, and inlines
     49     them in HTML pages, so that you can see all the (debug-intended)
     50     print statements in the page itself.
     51 
     52     There are two keys added to the environment to control this:
     53     ``environ['paste.printdebug_listeners']`` is a list of functions
     54     that will be called everytime something is printed.
     55 
     56     ``environ['paste.remove_printdebug']`` is a function that, if
     57     called, will disable printing of output for that request.
     58 
     59     If you have ``replace_stdout=True`` then stdout is replaced, not
     60     captured.
     61     """
     62 
     63     log_template = (
     64         '<pre style="width: 40%%; border: 2px solid #000; white-space: normal; '
     65         'background-color: #ffd; color: #000; float: right;">'
     66         '<b style="border-bottom: 1px solid #000">Log messages</b><br>'
     67         '%s</pre>')
     68 
     69     def __init__(self, app, global_conf=None, force_content_type=False,
     70                  print_wsgi_errors=True, replace_stdout=False):
     71         # @@: global_conf should be handled separately and only for
     72         # the entry point
     73         self.app = app
     74         self.force_content_type = force_content_type
     75         if isinstance(print_wsgi_errors, six.string_types):
     76             from paste.deploy.converters import asbool
     77             print_wsgi_errors = asbool(print_wsgi_errors)
     78         self.print_wsgi_errors = print_wsgi_errors
     79         self.replace_stdout = replace_stdout
     80         self._threaded_print_stdout = None
     81 
     82     def __call__(self, environ, start_response):
     83         global _threadedprint_installed
     84         if environ.get('paste.testing'):
     85             # In a testing environment this interception isn't
     86             # useful:
     87             return self.app(environ, start_response)
     88         if (not _threadedprint_installed
     89             or self._threaded_print_stdout is not sys.stdout):
     90             # @@: Not strictly threadsafe
     91             _threadedprint_installed = True
     92             threadedprint.install(leave_stdout=not self.replace_stdout)
     93             self._threaded_print_stdout = sys.stdout
     94         removed = []
     95         def remove_printdebug():
     96             removed.append(None)
     97         environ['paste.remove_printdebug'] = remove_printdebug
     98         logged = StringIO()
     99         listeners = [logged]
    100         environ['paste.printdebug_listeners'] = listeners
    101         if self.print_wsgi_errors:
    102             listeners.append(environ['wsgi.errors'])
    103         replacement_stdout = TeeFile(listeners)
    104         threadedprint.register(replacement_stdout)
    105         try:
    106             status, headers, body = wsgilib.intercept_output(
    107                 environ, self.app)
    108             if status is None:
    109                 # Some error occurred
    110                 status = '500 Server Error'
    111                 headers = [('Content-type', 'text/html')]
    112                 start_response(status, headers)
    113                 if not body:
    114                     body = 'An error occurred'
    115             content_type = response.header_value(headers, 'content-type')
    116             if (removed or
    117                 (not self.force_content_type and
    118                  (not content_type
    119                   or not content_type.startswith('text/html')))):
    120                 if replacement_stdout == logged:
    121                     # Then the prints will be lost, unless...
    122                     environ['wsgi.errors'].write(logged.getvalue())
    123                 start_response(status, headers)
    124                 return [body]
    125             response.remove_header(headers, 'content-length')
    126             body = self.add_log(body, logged.getvalue())
    127             start_response(status, headers)
    128             return [body]
    129         finally:
    130             threadedprint.deregister()
    131 
    132     _body_re = re.compile(r'<body[^>]*>', re.I)
    133     _explicit_re = re.compile(r'<pre\s*[^>]*id="paste-debug-prints".*?>',
    134                               re.I+re.S)
    135 
    136     def add_log(self, html, log):
    137         if not log:
    138             return html
    139         text = cgi.escape(log)
    140         text = text.replace('\n', '<br>')
    141         text = text.replace('  ', '&nbsp; ')
    142         match = self._explicit_re.search(html)
    143         if not match:
    144             text = self.log_template % text
    145             match = self._body_re.search(html)
    146         if not match:
    147             return text + html
    148         else:
    149             return html[:match.end()] + text + html[match.end():]
    150