Home | History | Annotate | Download | only in paste
      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 """
      5 A module of many disparate routines.
      6 """
      7 
      8 from __future__ import print_function
      9 
     10 # functions which moved to paste.request and paste.response
     11 # Deprecated around 15 Dec 2005
     12 from paste.request import get_cookies, parse_querystring, parse_formvars
     13 from paste.request import construct_url, path_info_split, path_info_pop
     14 from paste.response import HeaderDict, has_header, header_value, remove_header
     15 from paste.response import error_body_response, error_response, error_response_app
     16 
     17 from traceback import print_exception
     18 import six
     19 import sys
     20 from six.moves import cStringIO as StringIO
     21 from six.moves.urllib.parse import unquote, urlsplit
     22 import warnings
     23 
     24 __all__ = ['add_close', 'add_start_close', 'capture_output', 'catch_errors',
     25            'catch_errors_app', 'chained_app_iters', 'construct_url',
     26            'dump_environ', 'encode_unicode_app_iter', 'error_body_response',
     27            'error_response', 'get_cookies', 'has_header', 'header_value',
     28            'interactive', 'intercept_output', 'path_info_pop',
     29            'path_info_split', 'raw_interactive', 'send_file']
     30 
     31 class add_close(object):
     32     """
     33     An an iterable that iterates over app_iter, then calls
     34     close_func.
     35     """
     36 
     37     def __init__(self, app_iterable, close_func):
     38         self.app_iterable = app_iterable
     39         self.app_iter = iter(app_iterable)
     40         self.close_func = close_func
     41         self._closed = False
     42 
     43     def __iter__(self):
     44         return self
     45 
     46     def next(self):
     47         return self.app_iter.next()
     48 
     49     def close(self):
     50         self._closed = True
     51         if hasattr(self.app_iterable, 'close'):
     52             self.app_iterable.close()
     53         self.close_func()
     54 
     55     def __del__(self):
     56         if not self._closed:
     57             # We can't raise an error or anything at this stage
     58             print("Error: app_iter.close() was not called when finishing "
     59                 "WSGI request. finalization function %s not called"
     60                   % self.close_func, file=sys.stderr)
     61 
     62 class add_start_close(object):
     63     """
     64     An an iterable that iterates over app_iter, calls start_func
     65     before the first item is returned, then calls close_func at the
     66     end.
     67     """
     68 
     69     def __init__(self, app_iterable, start_func, close_func=None):
     70         self.app_iterable = app_iterable
     71         self.app_iter = iter(app_iterable)
     72         self.first = True
     73         self.start_func = start_func
     74         self.close_func = close_func
     75         self._closed = False
     76 
     77     def __iter__(self):
     78         return self
     79 
     80     def next(self):
     81         if self.first:
     82             self.start_func()
     83             self.first = False
     84         return next(self.app_iter)
     85     __next__ = next
     86 
     87     def close(self):
     88         self._closed = True
     89         if hasattr(self.app_iterable, 'close'):
     90             self.app_iterable.close()
     91         if self.close_func is not None:
     92             self.close_func()
     93 
     94     def __del__(self):
     95         if not self._closed:
     96             # We can't raise an error or anything at this stage
     97             print("Error: app_iter.close() was not called when finishing "
     98                 "WSGI request. finalization function %s not called"
     99                   % self.close_func, file=sys.stderr)
    100 
    101 class chained_app_iters(object):
    102 
    103     """
    104     Chains several app_iters together, also delegating .close() to each
    105     of them.
    106     """
    107 
    108     def __init__(self, *chained):
    109         self.app_iters = chained
    110         self.chained = [iter(item) for item in chained]
    111         self._closed = False
    112 
    113     def __iter__(self):
    114         return self
    115 
    116     def next(self):
    117         if len(self.chained) == 1:
    118             return self.chained[0].next()
    119         else:
    120             try:
    121                 return self.chained[0].next()
    122             except StopIteration:
    123                 self.chained.pop(0)
    124                 return self.next()
    125 
    126     def close(self):
    127         self._closed = True
    128         got_exc = None
    129         for app_iter in self.app_iters:
    130             try:
    131                 if hasattr(app_iter, 'close'):
    132                     app_iter.close()
    133             except:
    134                 got_exc = sys.exc_info()
    135         if got_exc:
    136             six.reraise(got_exc[0], got_exc[1], got_exc[2])
    137 
    138     def __del__(self):
    139         if not self._closed:
    140             # We can't raise an error or anything at this stage
    141             print("Error: app_iter.close() was not called when finishing "
    142                 "WSGI request. finalization function %s not called"
    143                   % self.close_func, file=sys.stderr)
    144 
    145 class encode_unicode_app_iter(object):
    146     """
    147     Encodes an app_iterable's unicode responses as strings
    148     """
    149 
    150     def __init__(self, app_iterable, encoding=sys.getdefaultencoding(),
    151                  errors='strict'):
    152         self.app_iterable = app_iterable
    153         self.app_iter = iter(app_iterable)
    154         self.encoding = encoding
    155         self.errors = errors
    156 
    157     def __iter__(self):
    158         return self
    159 
    160     def next(self):
    161         content = next(self.app_iter)
    162         if isinstance(content, six.text_type):
    163             content = content.encode(self.encoding, self.errors)
    164         return content
    165     __next__ = next
    166 
    167     def close(self):
    168         if hasattr(self.app_iterable, 'close'):
    169             self.app_iterable.close()
    170 
    171 def catch_errors(application, environ, start_response, error_callback,
    172                  ok_callback=None):
    173     """
    174     Runs the application, and returns the application iterator (which should be
    175     passed upstream).  If an error occurs then error_callback will be called with
    176     exc_info as its sole argument.  If no errors occur and ok_callback is given,
    177     then it will be called with no arguments.
    178     """
    179     try:
    180         app_iter = application(environ, start_response)
    181     except:
    182         error_callback(sys.exc_info())
    183         raise
    184     if type(app_iter) in (list, tuple):
    185         # These won't produce exceptions
    186         if ok_callback:
    187             ok_callback()
    188         return app_iter
    189     else:
    190         return _wrap_app_iter(app_iter, error_callback, ok_callback)
    191 
    192 class _wrap_app_iter(object):
    193 
    194     def __init__(self, app_iterable, error_callback, ok_callback):
    195         self.app_iterable = app_iterable
    196         self.app_iter = iter(app_iterable)
    197         self.error_callback = error_callback
    198         self.ok_callback = ok_callback
    199         if hasattr(self.app_iterable, 'close'):
    200             self.close = self.app_iterable.close
    201 
    202     def __iter__(self):
    203         return self
    204 
    205     def next(self):
    206         try:
    207             return self.app_iter.next()
    208         except StopIteration:
    209             if self.ok_callback:
    210                 self.ok_callback()
    211             raise
    212         except:
    213             self.error_callback(sys.exc_info())
    214             raise
    215 
    216 def catch_errors_app(application, environ, start_response, error_callback_app,
    217                      ok_callback=None, catch=Exception):
    218     """
    219     Like ``catch_errors``, except error_callback_app should be a
    220     callable that will receive *three* arguments -- ``environ``,
    221     ``start_response``, and ``exc_info``.  It should call
    222     ``start_response`` (*with* the exc_info argument!) and return an
    223     iterator.
    224     """
    225     try:
    226         app_iter = application(environ, start_response)
    227     except catch:
    228         return error_callback_app(environ, start_response, sys.exc_info())
    229     if type(app_iter) in (list, tuple):
    230         # These won't produce exceptions
    231         if ok_callback is not None:
    232             ok_callback()
    233         return app_iter
    234     else:
    235         return _wrap_app_iter_app(
    236             environ, start_response, app_iter,
    237             error_callback_app, ok_callback, catch=catch)
    238 
    239 class _wrap_app_iter_app(object):
    240 
    241     def __init__(self, environ, start_response, app_iterable,
    242                  error_callback_app, ok_callback, catch=Exception):
    243         self.environ = environ
    244         self.start_response = start_response
    245         self.app_iterable = app_iterable
    246         self.app_iter = iter(app_iterable)
    247         self.error_callback_app = error_callback_app
    248         self.ok_callback = ok_callback
    249         self.catch = catch
    250         if hasattr(self.app_iterable, 'close'):
    251             self.close = self.app_iterable.close
    252 
    253     def __iter__(self):
    254         return self
    255 
    256     def next(self):
    257         try:
    258             return self.app_iter.next()
    259         except StopIteration:
    260             if self.ok_callback:
    261                 self.ok_callback()
    262             raise
    263         except self.catch:
    264             if hasattr(self.app_iterable, 'close'):
    265                 try:
    266                     self.app_iterable.close()
    267                 except:
    268                     # @@: Print to wsgi.errors?
    269                     pass
    270             new_app_iterable = self.error_callback_app(
    271                 self.environ, self.start_response, sys.exc_info())
    272             app_iter = iter(new_app_iterable)
    273             if hasattr(new_app_iterable, 'close'):
    274                 self.close = new_app_iterable.close
    275             self.next = app_iter.next
    276             return self.next()
    277 
    278 def raw_interactive(application, path='', raise_on_wsgi_error=False,
    279                     **environ):
    280     """
    281     Runs the application in a fake environment.
    282     """
    283     assert "path_info" not in environ, "argument list changed"
    284     if raise_on_wsgi_error:
    285         errors = ErrorRaiser()
    286     else:
    287         errors = six.BytesIO()
    288     basic_environ = {
    289         # mandatory CGI variables
    290         'REQUEST_METHOD': 'GET',     # always mandatory
    291         'SCRIPT_NAME': '',           # may be empty if app is at the root
    292         'PATH_INFO': '',             # may be empty if at root of app
    293         'SERVER_NAME': 'localhost',  # always mandatory
    294         'SERVER_PORT': '80',         # always mandatory
    295         'SERVER_PROTOCOL': 'HTTP/1.0',
    296         # mandatory wsgi variables
    297         'wsgi.version': (1, 0),
    298         'wsgi.url_scheme': 'http',
    299         'wsgi.input': six.BytesIO(),
    300         'wsgi.errors': errors,
    301         'wsgi.multithread': False,
    302         'wsgi.multiprocess': False,
    303         'wsgi.run_once': False,
    304         }
    305     if path:
    306         (_, _, path_info, query, fragment) = urlsplit(str(path))
    307         path_info = unquote(path_info)
    308         # urlsplit returns unicode so coerce it back to str
    309         path_info, query = str(path_info), str(query)
    310         basic_environ['PATH_INFO'] = path_info
    311         if query:
    312             basic_environ['QUERY_STRING'] = query
    313     for name, value in environ.items():
    314         name = name.replace('__', '.')
    315         basic_environ[name] = value
    316     if ('SERVER_NAME' in basic_environ
    317         and 'HTTP_HOST' not in basic_environ):
    318         basic_environ['HTTP_HOST'] = basic_environ['SERVER_NAME']
    319     istream = basic_environ['wsgi.input']
    320     if isinstance(istream, bytes):
    321         basic_environ['wsgi.input'] = six.BytesIO(istream)
    322         basic_environ['CONTENT_LENGTH'] = len(istream)
    323     data = {}
    324     output = []
    325     headers_set = []
    326     headers_sent = []
    327     def start_response(status, headers, exc_info=None):
    328         if exc_info:
    329             try:
    330                 if headers_sent:
    331                     # Re-raise original exception only if headers sent
    332                     six.reraise(exc_info[0], exc_info[1], exc_info[2])
    333             finally:
    334                 # avoid dangling circular reference
    335                 exc_info = None
    336         elif headers_set:
    337             # You cannot set the headers more than once, unless the
    338             # exc_info is provided.
    339             raise AssertionError("Headers already set and no exc_info!")
    340         headers_set.append(True)
    341         data['status'] = status
    342         data['headers'] = headers
    343         return output.append
    344     app_iter = application(basic_environ, start_response)
    345     try:
    346         try:
    347             for s in app_iter:
    348                 if not isinstance(s, six.binary_type):
    349                     raise ValueError(
    350                         "The app_iter response can only contain bytes (not "
    351                         "unicode); got: %r" % s)
    352                 headers_sent.append(True)
    353                 if not headers_set:
    354                     raise AssertionError("Content sent w/o headers!")
    355                 output.append(s)
    356         except TypeError as e:
    357             # Typically "iteration over non-sequence", so we want
    358             # to give better debugging information...
    359             e.args = ((e.args[0] + ' iterable: %r' % app_iter),) + e.args[1:]
    360             raise
    361     finally:
    362         if hasattr(app_iter, 'close'):
    363             app_iter.close()
    364     return (data['status'], data['headers'], b''.join(output),
    365             errors.getvalue())
    366 
    367 class ErrorRaiser(object):
    368 
    369     def flush(self):
    370         pass
    371 
    372     def write(self, value):
    373         if not value:
    374             return
    375         raise AssertionError(
    376             "No errors should be written (got: %r)" % value)
    377 
    378     def writelines(self, seq):
    379         raise AssertionError(
    380             "No errors should be written (got lines: %s)" % list(seq))
    381 
    382     def getvalue(self):
    383         return ''
    384 
    385 def interactive(*args, **kw):
    386     """
    387     Runs the application interatively, wrapping `raw_interactive` but
    388     returning the output in a formatted way.
    389     """
    390     status, headers, content, errors = raw_interactive(*args, **kw)
    391     full = StringIO()
    392     if errors:
    393         full.write('Errors:\n')
    394         full.write(errors.strip())
    395         full.write('\n----------end errors\n')
    396     full.write(status + '\n')
    397     for name, value in headers:
    398         full.write('%s: %s\n' % (name, value))
    399     full.write('\n')
    400     full.write(content)
    401     return full.getvalue()
    402 interactive.proxy = 'raw_interactive'
    403 
    404 def dump_environ(environ, start_response):
    405     """
    406     Application which simply dumps the current environment
    407     variables out as a plain text response.
    408     """
    409     output = []
    410     keys = list(environ.keys())
    411     keys.sort()
    412     for k in keys:
    413         v = str(environ[k]).replace("\n","\n    ")
    414         output.append("%s: %s\n" % (k, v))
    415     output.append("\n")
    416     content_length = environ.get("CONTENT_LENGTH", '')
    417     if content_length:
    418         output.append(environ['wsgi.input'].read(int(content_length)))
    419         output.append("\n")
    420     output = "".join(output)
    421     if six.PY3:
    422         output = output.encode('utf8')
    423     headers = [('Content-Type', 'text/plain'),
    424                ('Content-Length', str(len(output)))]
    425     start_response("200 OK", headers)
    426     return [output]
    427 
    428 def send_file(filename):
    429     warnings.warn(
    430         "wsgilib.send_file has been moved to paste.fileapp.FileApp",
    431         DeprecationWarning, 2)
    432     from paste import fileapp
    433     return fileapp.FileApp(filename)
    434 
    435 def capture_output(environ, start_response, application):
    436     """
    437     Runs application with environ and start_response, and captures
    438     status, headers, and body.
    439 
    440     Sends status and header, but *not* body.  Returns (status,
    441     headers, body).  Typically this is used like:
    442 
    443     .. code-block:: python
    444 
    445         def dehtmlifying_middleware(application):
    446             def replacement_app(environ, start_response):
    447                 status, headers, body = capture_output(
    448                     environ, start_response, application)
    449                 content_type = header_value(headers, 'content-type')
    450                 if (not content_type
    451                     or not content_type.startswith('text/html')):
    452                     return [body]
    453                 body = re.sub(r'<.*?>', '', body)
    454                 return [body]
    455             return replacement_app
    456 
    457     """
    458     warnings.warn(
    459         'wsgilib.capture_output has been deprecated in favor '
    460         'of wsgilib.intercept_output',
    461         DeprecationWarning, 2)
    462     data = []
    463     output = StringIO()
    464     def replacement_start_response(status, headers, exc_info=None):
    465         if data:
    466             data[:] = []
    467         data.append(status)
    468         data.append(headers)
    469         start_response(status, headers, exc_info)
    470         return output.write
    471     app_iter = application(environ, replacement_start_response)
    472     try:
    473         for item in app_iter:
    474             output.write(item)
    475     finally:
    476         if hasattr(app_iter, 'close'):
    477             app_iter.close()
    478     if not data:
    479         data.append(None)
    480     if len(data) < 2:
    481         data.append(None)
    482     data.append(output.getvalue())
    483     return data
    484 
    485 def intercept_output(environ, application, conditional=None,
    486                      start_response=None):
    487     """
    488     Runs application with environ and captures status, headers, and
    489     body.  None are sent on; you must send them on yourself (unlike
    490     ``capture_output``)
    491 
    492     Typically this is used like:
    493 
    494     .. code-block:: python
    495 
    496         def dehtmlifying_middleware(application):
    497             def replacement_app(environ, start_response):
    498                 status, headers, body = intercept_output(
    499                     environ, application)
    500                 start_response(status, headers)
    501                 content_type = header_value(headers, 'content-type')
    502                 if (not content_type
    503                     or not content_type.startswith('text/html')):
    504                     return [body]
    505                 body = re.sub(r'<.*?>', '', body)
    506                 return [body]
    507             return replacement_app
    508 
    509     A third optional argument ``conditional`` should be a function
    510     that takes ``conditional(status, headers)`` and returns False if
    511     the request should not be intercepted.  In that case
    512     ``start_response`` will be called and ``(None, None, app_iter)``
    513     will be returned.  You must detect that in your code and return
    514     the app_iter, like:
    515 
    516     .. code-block:: python
    517 
    518         def dehtmlifying_middleware(application):
    519             def replacement_app(environ, start_response):
    520                 status, headers, body = intercept_output(
    521                     environ, application,
    522                     lambda s, h: header_value(headers, 'content-type').startswith('text/html'),
    523                     start_response)
    524                 if status is None:
    525                     return body
    526                 start_response(status, headers)
    527                 body = re.sub(r'<.*?>', '', body)
    528                 return [body]
    529             return replacement_app
    530     """
    531     if conditional is not None and start_response is None:
    532         raise TypeError(
    533             "If you provide conditional you must also provide "
    534             "start_response")
    535     data = []
    536     output = StringIO()
    537     def replacement_start_response(status, headers, exc_info=None):
    538         if conditional is not None and not conditional(status, headers):
    539             data.append(None)
    540             return start_response(status, headers, exc_info)
    541         if data:
    542             data[:] = []
    543         data.append(status)
    544         data.append(headers)
    545         return output.write
    546     app_iter = application(environ, replacement_start_response)
    547     if data[0] is None:
    548         return (None, None, app_iter)
    549     try:
    550         for item in app_iter:
    551             output.write(item)
    552     finally:
    553         if hasattr(app_iter, 'close'):
    554             app_iter.close()
    555     if not data:
    556         data.append(None)
    557     if len(data) < 2:
    558         data.append(None)
    559     data.append(output.getvalue())
    560     return data
    561 
    562 ## Deprecation warning wrapper:
    563 
    564 class ResponseHeaderDict(HeaderDict):
    565 
    566     def __init__(self, *args, **kw):
    567         warnings.warn(
    568             "The class wsgilib.ResponseHeaderDict has been moved "
    569             "to paste.response.HeaderDict",
    570             DeprecationWarning, 2)
    571         HeaderDict.__init__(self, *args, **kw)
    572 
    573 def _warn_deprecated(new_func):
    574     new_name = new_func.func_name
    575     new_path = new_func.func_globals['__name__'] + '.' + new_name
    576     def replacement(*args, **kw):
    577         warnings.warn(
    578             "The function wsgilib.%s has been moved to %s"
    579             % (new_name, new_path),
    580             DeprecationWarning, 2)
    581         return new_func(*args, **kw)
    582     try:
    583         replacement.func_name = new_func.func_name
    584     except:
    585         pass
    586     return replacement
    587 
    588 # Put warnings wrapper in place for all public functions that
    589 # were imported from elsewhere:
    590 
    591 for _name in __all__:
    592     _func = globals()[_name]
    593     if (hasattr(_func, 'func_globals')
    594         and _func.func_globals['__name__'] != __name__):
    595         globals()[_name] = _warn_deprecated(_func)
    596 
    597 if __name__ == '__main__':
    598     import doctest
    599     doctest.testmod()
    600 
    601