Back to index

moin  1.9.0~rc2
test.py
Go to the documentation of this file.
00001 # -*- coding: utf-8 -*-
00002 """
00003     werkzeug.test
00004     ~~~~~~~~~~~~~
00005 
00006     This module implements a client to WSGI applications for testing.
00007 
00008     :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
00009     :license: BSD, see LICENSE for more details.
00010 """
00011 import sys
00012 import urlparse
00013 import mimetypes
00014 from time import time
00015 from random import random
00016 from itertools import chain
00017 from tempfile import TemporaryFile
00018 from cStringIO import StringIO
00019 from cookielib import CookieJar
00020 from urllib2 import Request as U2Request
00021 
00022 from werkzeug._internal import _empty_stream
00023 from werkzeug.wrappers import BaseRequest
00024 from werkzeug.utils import create_environ, run_wsgi_app, get_current_url, \
00025      url_encode, url_decode, FileStorage, get_host
00026 from werkzeug.datastructures import FileMultiDict, MultiDict, \
00027      CombinedMultiDict, Headers
00028 
00029 
00030 def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
00031                             boundary=None, charset='utf-8'):
00032     """Encode a dict of values (either strings or file descriptors or
00033     :class:`FileStorage` objects.) into a multipart encoded string stored
00034     in a file descriptor.
00035     """
00036     if boundary is None:
00037         boundary = '---------------WerkzeugFormPart_%s%s' % (time(), random())
00038     _closure = [StringIO(), 0, False]
00039 
00040     if use_tempfile:
00041         def write(string):
00042             stream, total_length, on_disk = _closure
00043             if on_disk:
00044                 stream.write(string)
00045             else:
00046                 length = len(string)
00047                 if length + _closure[1] <= threshold:
00048                     stream.write(string)
00049                 else:
00050                     new_stream = TemporaryFile('wb+')
00051                     new_stream.write(stream.getvalue())
00052                     new_stream.write(string)
00053                     _closure[0] = new_stream
00054                     _closure[2] = True
00055                 _closure[1] = total_length + length
00056     else:
00057         write = _closure[0].write
00058 
00059     if not isinstance(values, MultiDict):
00060         values = MultiDict(values)
00061 
00062     for key, values in values.iterlists():
00063         for value in values:
00064             write('--%s\r\nContent-Disposition: form-data; name="%s"' %
00065                   (boundary, key))
00066             reader = getattr(value, 'read', None)
00067             if reader is not None:
00068                 filename = getattr(value, 'filename',
00069                                    getattr(value, 'name', None))
00070                 content_type = getattr(value, 'content_type', None)
00071                 if content_type is None:
00072                     content_type = filename and \
00073                         mimetypes.guess_type(filename)[0] or \
00074                         'application/octet-stream'
00075                 if filename is not None:
00076                     write('; filename="%s"\r\n' % filename)
00077                 else:
00078                     write('\r\n')
00079                 write('Content-Type: %s\r\n\r\n' % content_type)
00080                 while 1:
00081                     chunk = reader(16384)
00082                     if not chunk:
00083                         break
00084                     write(chunk)
00085             else:
00086                 if isinstance(value, unicode):
00087                     value = value.encode(charset)
00088                 write('\r\n\r\n' + value)
00089             write('\r\n')
00090     write('--%s--\r\n' % boundary)
00091 
00092     length = int(_closure[0].tell())
00093     _closure[0].seek(0)
00094     return _closure[0], length, boundary
00095 
00096 
00097 def encode_multipart(values, boundary=None, charset='utf-8'):
00098     """Like `stream_encode_multipart` but returns a tuple in the form
00099     (``boundary``, ``data``) where data is a bytestring.
00100     """
00101     stream, length, boundary = stream_encode_multipart(
00102         values, use_tempfile=False, boundary=boundary, charset=charset)
00103     return boundary, stream.read()
00104 
00105 
00106 def File(fd, filename=None, mimetype=None):
00107     """Backwards compat."""
00108     from warnings import warn
00109     warn(DeprecationWarning('werkzeug.test.File is deprecated, use the '
00110                             'EnvironBuilder or FileStorage instead'))
00111     return FileStorage(fd, filename=filename, content_type=mimetype)
00112 
00113 
00114 class _TestCookieHeaders(object):
00115     """A headers adapter for cookielib
00116     """
00117 
00118     def __init__(self, headers):
00119         self.headers = headers
00120 
00121     def getheaders(self, name):
00122         headers = []
00123         name = name.lower()
00124         for k, v in self.headers:
00125             if k.lower() == name:
00126                 headers.append(v)
00127         return headers
00128 
00129 
00130 class _TestCookieResponse(object):
00131     """Something that looks like a httplib.HTTPResponse, but is actually just an
00132     adapter for our test responses to make them available for cookielib.
00133     """
00134 
00135     def __init__(self, headers):
00136         self.headers = _TestCookieHeaders(headers)
00137 
00138     def info(self):
00139         return self.headers
00140 
00141 
00142 class _TestCookieJar(CookieJar):
00143     """A cookielib.CookieJar modified to inject and read cookie headers from
00144     and to wsgi environments, and wsgi application responses.
00145     """
00146 
00147     def inject_wsgi(self, environ):
00148         """Inject the cookies as client headers into the server's wsgi
00149         environment.
00150         """
00151         cvals = []
00152         for cookie in self:
00153             cvals.append('%s=%s' % (cookie.name, cookie.value))
00154         if cvals:
00155             environ['HTTP_COOKIE'] = ','.join(cvals)
00156 
00157     def extract_wsgi(self, environ, headers):
00158         """Extract the server's set-cookie headers as cookies into the
00159         cookie jar.
00160         """
00161         self.extract_cookies(
00162             _TestCookieResponse(headers),
00163             U2Request(get_current_url(environ)),
00164         )
00165 
00166 
00167 def _iter_data(data):
00168     """Iterates over a dict or multidict yielding all keys and values.
00169     This is used to iterate over the data passed to the
00170     :class:`EnvironBuilder`.
00171     """
00172     if isinstance(data, MultiDict):
00173         for key, values in data.iterlists():
00174             for value in values:
00175                 yield key, value
00176     else:
00177         for item in data.iteritems():
00178             yield item
00179 
00180 
00181 class EnvironBuilder(object):
00182     """This class can be used to conveniently create a WSGI environment
00183     for testing purposes.  It can be used to quickly create WSGI environments
00184     or request objects from arbitrary data.
00185 
00186     The signature of this class is also used in some other places as of
00187     Werkzeug 0.5 (:func:`create_environ`, :meth:`BaseResponse.from_values`,
00188     :meth:`Client.open`).  Because of this most of the functionality is
00189     available through the constructor alone.
00190 
00191     Files and regular form data can be manipulated independently of each
00192     other with the :attr:`form` and :attr:`files` attributes, but are
00193     passed with the same argument to the constructor: `data`.
00194 
00195     `data` can be any of these values:
00196 
00197     -   a `str`: If it's a string it is converted into a :attr:`input_stream`,
00198         the :attr:`content_length` is set and you have to provide a
00199         :attr:`content_type`.
00200     -   a `dict`: If it's a dict the keys have to be strings and the values
00201         and of the following objects:
00202 
00203         -   a :class:`file`-like object.  These are converted into
00204             :class:`FileStorage` objects automatically.
00205         -   a tuple.  The :meth:`~FileMultiDict.add_file` method is called
00206             with the tuple items as positional arguments.
00207 
00208     :param path: the path of the request.  In the WSGI environment this will
00209                  end up as `PATH_INFO`.  If the `query_string` is not defined
00210                  and there is a question mark in the `path` everything after
00211                  it is used as query string.
00212     :param base_url: the base URL is a URL that is used to extract the WSGI
00213                      URL scheme, host (server name + server port) and the
00214                      script root (`SCRIPT_NAME`).
00215     :param query_string: an optional string or dict with URL parameters.
00216     :param method: the HTTP method to use, defaults to `GET`.
00217     :param input_stream: an optional input stream.  Do not specify this and
00218                          `data`.  As soon as an input stream is set you can't
00219                          modify :attr:`args` and :attr:`files` unless you
00220                          set the :attr:`input_stream` to `None` again.
00221     :param content_type: The content type for the request.  As of 0.5 you
00222                          don't have to provide this when specifying files
00223                          and form data via `data`.
00224     :param content_length: The content length for the request.  You don't
00225                            have to specify this when providing data via
00226                            `data`.
00227     :param errors_stream: an optional error stream that is used for
00228                           `wsgi.errors`.  Defaults to :data:`stderr`.
00229     :param multithread: controls `wsgi.multithread`.  Defaults to `False`.
00230     :param multiprocess: controls `wsgi.multiprocess`.  Defaults to `False`.
00231     :param run_once: controls `wsgi.run_once`.  Defaults to `False`.
00232     :param headers: an optional list or :class:`Headers` object of headers.
00233     :param data: a string or dict of form data.  See explanation above.
00234     :param environ_base: an optional dict of environment defaults.
00235     :param environ_overrides: an optional dict of environment overrides.
00236     :param charset: the charset used to encode unicode data.
00237     """
00238 
00239     #: the server protocol to use.  defaults to HTTP/1.1
00240     server_protocol = 'HTTP/1.1'
00241 
00242     #: the wsgi version to use.  defaults to (1, 0)
00243     wsgi_version = (1, 0)
00244 
00245     #: the default request class for :meth:`get_request`
00246     request_class = BaseRequest
00247 
00248     def __init__(self, path='/', base_url=None, query_string=None,
00249                  method='GET', input_stream=None, content_type=None,
00250                  content_length=None, errors_stream=None, multithread=False,
00251                  multiprocess=False, run_once=False, headers=None, data=None,
00252                  environ_base=None, environ_overrides=None, charset='utf-8'):
00253         if query_string is None and '?' in path:
00254             path, query_string = path.split('?', 1)
00255         self.charset = charset
00256         self.path = path
00257         self.base_url = base_url
00258         if isinstance(query_string, basestring):
00259             self.query_string = query_string
00260         else:
00261             if query_string is None:
00262                 query_string = MultiDict()
00263             elif not isinstance(query_string, MultiDict):
00264                 query_string = MultiDict(query_string)
00265             self.args = query_string
00266         self.method = method
00267         if headers is None:
00268             headers = Headers()
00269         elif not isinstance(headers, Headers):
00270             headers = Headers(headers)
00271         self.headers = headers
00272         self.content_type = content_type
00273         if errors_stream is None:
00274             errors_stream = sys.stderr
00275         self.errors_stream = errors_stream
00276         self.multithread = multithread
00277         self.multiprocess = multiprocess
00278         self.run_once = run_once
00279         self.environ_base = environ_base
00280         self.environ_overrides = environ_overrides
00281         self.input_stream = input_stream
00282         self.content_length = content_length
00283         self.closed = False
00284 
00285         if data:
00286             if input_stream is not None:
00287                 raise TypeError('can\'t provide input stream and data')
00288             if isinstance(data, basestring):
00289                 self.input_stream = StringIO(data)
00290                 if self.content_length is None:
00291                     self.content_length = len(data)
00292             else:
00293                 for key, value in _iter_data(data):
00294                     if isinstance(value, (tuple, dict)) or \
00295                        hasattr(value, 'read'):
00296                         self._add_file_from_data(key, value)
00297                     else:
00298                         self.form[key] = value
00299 
00300     def _add_file_from_data(self, key, value):
00301         """Called in the EnvironBuilder to add files from the data dict."""
00302         if isinstance(value, tuple):
00303             self.files.add_file(key, *value)
00304         elif isinstance(value, dict):
00305             from warnings import warn
00306             warn(DeprecationWarning('it\'s no longer possible to pass dicts '
00307                                     'as `data`.  Use tuples or FileStorage '
00308                                     'objects intead'), stacklevel=2)
00309             args = v
00310             value = dict(value)
00311             mimetype = value.pop('mimetype', None)
00312             if mimetype is not None:
00313                 value['content_type'] = mimetype
00314             self.files.add_file(key, **value)
00315         else:
00316             self.files.add_file(key, value)
00317 
00318     def _get_base_url(self):
00319         return urlparse.urlunsplit((self.url_scheme, self.host,
00320                                     self.script_root, '', '')).rstrip('/') + '/'
00321 
00322     def _set_base_url(self, value):
00323         if value is None:
00324             scheme = 'http'
00325             netloc = 'localhost'
00326             scheme = 'http'
00327             script_root = ''
00328         else:
00329             scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(value)
00330             if qs or anchor:
00331                 raise ValueError('base url must not contain a query string '
00332                                  'or fragment')
00333         self.script_root = script_root.rstrip('/')
00334         self.host = netloc
00335         self.url_scheme = scheme
00336 
00337     base_url = property(_get_base_url, _set_base_url, doc='''
00338         The base URL is a URL that is used to extract the WSGI
00339         URL scheme, host (server name + server port) and the
00340         script root (`SCRIPT_NAME`).''')
00341     del _get_base_url, _set_base_url
00342 
00343     def _get_content_type(self):
00344         ct = self.headers.get('Content-Type')
00345         if ct is None and not self._input_stream:
00346             if self.method in ('POST', 'PUT'):
00347                 if self._files:
00348                     return 'multipart/form-data'
00349                 return 'application/x-www-form-urlencoded'
00350             return None
00351         return ct
00352 
00353     def _set_content_type(self, value):
00354         if value is None:
00355             self.headers.pop('Content-Type', None)
00356         else:
00357             self.headers['Content-Type'] = value
00358 
00359     content_type = property(_get_content_type, _set_content_type, doc='''
00360         The content type for the request.  Reflected from and to the
00361         :attr:`headers`.  Do not set if you set :attr:`files` or
00362         :attr:`form` for auto detection.''')
00363     del _get_content_type, _set_content_type
00364 
00365     def _get_content_length(self):
00366         return self.headers.get('Content-Length', type=int)
00367 
00368     def _set_content_length(self, value):
00369         if value is None:
00370             self.headers.pop('Content-Length', None)
00371         else:
00372             self.headers['Content-Length'] = str(value)
00373 
00374     content_length = property(_get_content_length, _set_content_length, doc='''
00375         The content length as integer.  Reflected from and to the
00376         :attr:`headers`.  Do not set if you set :attr:`files` or
00377         :attr:`form` for auto detection.''')
00378     del _get_content_length, _set_content_length
00379 
00380     def form_property(name, storage, doc):
00381         key = '_' + name
00382         def getter(self):
00383             if self._input_stream is not None:
00384                 raise AttributeError('an input stream is defined')
00385             rv = getattr(self, key)
00386             if rv is None:
00387                 rv = storage()
00388                 setattr(self, key, rv)
00389             return rv
00390         def setter(self, value):
00391             self._input_stream = None
00392             setattr(self, key, value)
00393         return property(getter, setter, doc)
00394 
00395     form = form_property('form', MultiDict, doc='''
00396         A :class:`MultiDict` of form values.''')
00397     files = form_property('files', FileMultiDict, doc='''
00398         A :class:`FileMultiDict` of uploaded files.  You can use the
00399         :meth:`~FileMultiDict.add_file` method to add new files to the
00400         dict.''')
00401     del form_property
00402 
00403     def _get_input_stream(self):
00404         return self._input_stream
00405 
00406     def _set_input_stream(self, value):
00407         self._input_stream = value
00408         self._form = self._files = None
00409 
00410     input_stream = property(_get_input_stream, _set_input_stream, doc='''
00411         An optional input stream.  If you set this it will clear
00412         :attr:`form` and :attr:`files`.''')
00413     del _get_input_stream, _set_input_stream
00414 
00415     def _get_query_string(self):
00416         if self._query_string is None:
00417             if self._args is not None:
00418                 return url_encode(self._args, charset=self.charset)
00419             return ''
00420         return self._query_string
00421 
00422     def _set_query_string(self, value):
00423         self._query_string = value
00424         self._args = None
00425 
00426     query_string = property(_get_query_string, _set_query_string, doc='''
00427         The query string.  If you set this to a string :attr:`args` will
00428         no longer be available.''')
00429     del _get_query_string, _set_query_string
00430 
00431     def _get_args(self):
00432         if self._query_string is not None:
00433             raise AttributeError('a query string is defined')
00434         if self._args is None:
00435             self._args = MultiDict()
00436         return self._args
00437 
00438     def _set_args(self, value):
00439         self._query_string = None
00440         self._args = value
00441 
00442     args = property(_get_args, _set_args, doc='''
00443         The URL arguments as :class:`MultiDict`.''')
00444     del _get_args, _set_args
00445 
00446     @property
00447     def server_name(self):
00448         """The server name (read-only, use :attr:`host` to set)"""
00449         return self.host.split(':', 1)[0]
00450 
00451     @property
00452     def server_port(self):
00453         """The server port as integer (read-only, use :attr:`host` to set)"""
00454         pieces = self.host.split(':', 1)
00455         if len(pieces) == 2 and pieces[1].isdigit():
00456             return int(pieces[1])
00457         elif self.url_scheme == 'https':
00458             return 443
00459         return 80
00460 
00461     def __del__(self):
00462         self.close()
00463 
00464     def close(self):
00465         """Closes all files.  If you put real :class:`file` objects into the
00466         :attr:`files` dict you can call this method to automatically close
00467         them all in one go.
00468         """
00469         if self.closed:
00470             return
00471         try:
00472             files = self.files.itervalues()
00473         except AttributeError:
00474             files = ()
00475         for f in files:
00476             try:
00477                 f.close()
00478             except Exception, e:
00479                 pass
00480         self.closed = True
00481 
00482     def get_environ(self):
00483         """Return the built environ."""
00484         input_stream = self.input_stream
00485         content_length = self.content_length
00486         content_type = self.content_type
00487 
00488         if input_stream is not None:
00489             start_pos = input_stream.tell()
00490             input_stream.seek(0, 2)
00491             end_pos = input_stream.tell()
00492             input_stream.seek(start_pos)
00493             content_length = end_pos - start_pos
00494         elif content_type == 'multipart/form-data':
00495             values = CombinedMultiDict([self.form, self.files])
00496             input_stream, content_length, boundary = \
00497                 stream_encode_multipart(values, charset=self.charset)
00498             content_type += '; boundary="%s"' % boundary
00499         elif content_type == 'application/x-www-form-urlencoded':
00500             values = url_encode(self.form, charset=self.charset)
00501             content_length = len(values)
00502             input_stream = StringIO(values)
00503         else:
00504             input_stream = _empty_stream
00505 
00506         result = {}
00507         if self.environ_base:
00508             result.update(self.environ_base)
00509 
00510         def _encode(x):
00511             if isinstance(x, unicode):
00512                 return x.encode(self.charset)
00513             return x
00514 
00515         result.update({
00516             'REQUEST_METHOD':       self.method,
00517             'SCRIPT_NAME':          _encode(self.script_root),
00518             'PATH_INFO':            _encode(self.path),
00519             'QUERY_STRING':         self.query_string,
00520             'SERVER_NAME':          self.server_name,
00521             'SERVER_PORT':          str(self.server_port),
00522             'HTTP_HOST':            self.host,
00523             'SERVER_PROTOCOL':      self.server_protocol,
00524             'CONTENT_TYPE':         content_type or '',
00525             'CONTENT_LENGTH':       str(content_length or '0'),
00526             'wsgi.version':         self.wsgi_version,
00527             'wsgi.url_scheme':      self.url_scheme,
00528             'wsgi.input':           input_stream,
00529             'wsgi.errors':          self.errors_stream,
00530             'wsgi.multithread':     self.multithread,
00531             'wsgi.multiprocess':    self.multiprocess,
00532             'wsgi.run_once':        self.run_once
00533         })
00534         for key, value in self.headers.to_list(self.charset):
00535             result['HTTP_%s' % key.upper().replace('-', '_')] = value
00536         if self.environ_overrides:
00537             result.update(self.environ_overrides)
00538         return result
00539 
00540     def get_request(self, cls=None):
00541         """Returns a request with the data.  If the request class is not
00542         specified :attr:`request_class` is used.
00543 
00544         :param cls: The request wrapper to use.
00545         """
00546         if cls is None:
00547             cls = self.request_class
00548         return cls(self.get_environ())
00549 
00550 
00551 class Client(object):
00552     """This class allows to send requests to a wrapped application.
00553 
00554     The response wrapper can be a class or factory function that takes
00555     three arguments: app_iter, status and headers.  The default response
00556     wrapper just returns a tuple.
00557 
00558     Example::
00559 
00560         class ClientResponse(BaseResponse):
00561             ...
00562 
00563         client = Client(MyApplication(), response_wrapper=ClientResponse)
00564 
00565     The use_cookies parameter indicates whether cookies should be stored and
00566     sent for subsequent requests. This is True by default, but passing False
00567     will disable this behaviour.
00568 
00569     .. versionadded:: 0.5
00570        `use_cookies` is new in this version.  Older versions did not provide
00571        builtin cookie support.
00572     """
00573 
00574     def __init__(self, application, response_wrapper=None, use_cookies=True):
00575         self.application = application
00576         if response_wrapper is None:
00577             response_wrapper = lambda a, s, h: (a, s, h)
00578         self.response_wrapper = response_wrapper
00579         if use_cookies:
00580             self.cookie_jar = _TestCookieJar()
00581         else:
00582             self.cookie_jar = None
00583 
00584     def open(self, *args, **kwargs):
00585         """Takes the same arguments as the :class:`EnvironBuilder` class with
00586         some additions:  You can provide a :class:`EnvironBuilder` or a WSGI
00587         environment as only argument instead of the :class:`EnvironBuilder`
00588         arguments and two optional keyword arguments (`as_tuple`, `buffered`)
00589         that change the type of the return value or the way the application is
00590         executed.
00591 
00592         .. versionchanged:: 0.5
00593            If a dict is provided as file in the dict for the `data` parameter
00594            the content type has to be called `content_type` now instead of
00595            `mimetype`.  This change was made for consistency with
00596            :class:`werkzeug.FileWrapper`.
00597 
00598             The `follow_redirects` parameter was added to :func:`open`.
00599 
00600         Additional parameters:
00601 
00602         :param as_tuple: Returns a tuple in the form ``(environ, result)``
00603         :param buffered: Set this to true to buffer the application run.
00604                          This will automatically close the application for
00605                          you as well.
00606         :param follow_redirects: Set this to True if the `Client` should
00607                                  follow HTTP redirects.
00608         """
00609         as_tuple = kwargs.pop('as_tuple', False)
00610         buffered = kwargs.pop('buffered', False)
00611         follow_redirects = kwargs.pop('follow_redirects', False)
00612         environ = None
00613         if not kwargs and len(args) == 1:
00614             if isinstance(args[0], EnvironBuilder):
00615                 environ = args[0].get_environ()
00616             elif isinstance(args[0], dict):
00617                 environ = args[0]
00618         if environ is None:
00619             builder = EnvironBuilder(*args, **kwargs)
00620             try:
00621                 environ = builder.get_environ()
00622             finally:
00623                 builder.close()
00624 
00625         if self.cookie_jar is not None:
00626             self.cookie_jar.inject_wsgi(environ)
00627         rv = run_wsgi_app(self.application, environ, buffered=buffered)
00628         if self.cookie_jar is not None:
00629             self.cookie_jar.extract_wsgi(environ, rv[2])
00630 
00631         # handle redirects
00632         redirect_chain = []
00633         status_code = int(rv[1].split(None, 1)[0])
00634         while status_code in (301, 302, 303, 305, 307) and follow_redirects:
00635             redirect = dict(rv[2])['Location']
00636             host = get_host(create_environ('/', redirect))
00637             if get_host(environ).split(':', 1)[0] != host:
00638                 raise RuntimeError('%r does not support redirect to '
00639                                    'external targets' % self.__class__)
00640 
00641             scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect)
00642             redirect_chain.append((redirect, status_code))
00643 
00644             kwargs.update({
00645                 'base_url':         urlparse.urlunsplit((scheme, host,
00646                                     script_root, '', '')).rstrip('/') + '/',
00647                 'query_string':     qs,
00648                 'as_tuple':         as_tuple,
00649                 'buffered':         buffered,
00650                 'follow_redirects': False
00651             })
00652             rv = self.open(*args, **kwargs)
00653             status_code = int(rv[1].split(None, 1)[0])
00654 
00655             # Prevent loops
00656             if redirect_chain[-1] in redirect_chain[0:-1]:
00657                 break
00658 
00659         response = self.response_wrapper(*rv)
00660         if as_tuple:
00661             return environ, response
00662         return response
00663 
00664     def get(self, *args, **kw):
00665         """Like open but method is enforced to GET."""
00666         kw['method'] = 'GET'
00667         return self.open(*args, **kw)
00668 
00669     def post(self, *args, **kw):
00670         """Like open but method is enforced to POST."""
00671         kw['method'] = 'POST'
00672         return self.open(*args, **kw)
00673 
00674     def head(self, *args, **kw):
00675         """Like open but method is enforced to HEAD."""
00676         kw['method'] = 'HEAD'
00677         return self.open(*args, **kw)
00678 
00679     def put(self, *args, **kw):
00680         """Like open but method is enforced to PUT."""
00681         kw['method'] = 'PUT'
00682         return self.open(*args, **kw)
00683 
00684     def delete(self, *args, **kw):
00685         """Like open but method is enforced to DELETE."""
00686         kw['method'] = 'DELETE'
00687         return self.open(*args, **kw)
00688 
00689     def __repr__(self):
00690         return '<%s %r>' % (
00691             self.__class__.__name__,
00692             self.application
00693         )
00694 
00695 
00696 def create_environ(*args, **kwargs):
00697     """Create a new WSGI environ dict based on the values passed.  The first
00698     parameter should be the path of the request which defaults to '/'.  The
00699     second one can either be an absolute path (in that case the host is
00700     localhost:80) or a full path to the request with scheme, netloc port and
00701     the path to the script.
00702 
00703     This accepts the same arguments as the :class:`EnvironBuilder`
00704     constructor.
00705 
00706     .. versionchanged:: 0.5
00707        This function is now a thin wrapper over :class:`EnvironBuilder` which
00708        was added in 0.5.  The `headers`, `environ_base`, `environ_overrides`
00709        and `charset` parameters were added.
00710     """
00711     builder = EnvironBuilder(*args, **kwargs)
00712     try:
00713         return builder.get_environ()
00714     finally:
00715         builder.close()
00716 
00717 
00718 def run_wsgi_app(app, environ, buffered=False):
00719     """Return a tuple in the form (app_iter, status, headers) of the
00720     application output.  This works best if you pass it an application that
00721     returns an iterator all the time.
00722 
00723     Sometimes applications may use the `write()` callable returned
00724     by the `start_response` function.  This tries to resolve such edge
00725     cases automatically.  But if you don't get the expected output you
00726     should set `buffered` to `True` which enforces buffering.
00727 
00728     If passed an invalid WSGI application the behavior of this function is
00729     undefined.  Never pass non-conforming WSGI applications to this function.
00730 
00731     :param app: the application to execute.
00732     :param buffered: set to `True` to enforce buffering.
00733     :return: tuple in the form ``(app_iter, status, headers)``
00734     """
00735     response = []
00736     buffer = []
00737 
00738     def start_response(status, headers, exc_info=None):
00739         if exc_info is not None:
00740             raise exc_info[0], exc_info[1], exc_info[2]
00741         response[:] = [status, headers]
00742         return buffer.append
00743 
00744     app_iter = app(environ, start_response)
00745 
00746     # when buffering we emit the close call early and conver the
00747     # application iterator into a regular list
00748     if buffered:
00749         close_func = getattr(app_iter, 'close', None)
00750         try:
00751             app_iter = list(app_iter)
00752         finally:
00753             if close_func is not None:
00754                 close_func()
00755 
00756     # otherwise we iterate the application iter until we have
00757     # a response, chain the already received data with the already
00758     # collected data and wrap it in a new `ClosingIterator` if
00759     # we have a close callable.
00760     else:
00761         while not response:
00762             buffer.append(app_iter.next())
00763         if buffer:
00764             app_iter = chain(buffer, app_iter)
00765             close_func = getattr(app_iter, 'close', None)
00766             if close_func is not None:
00767                 app_iter = ClosingIterator(app_iter, close_func)
00768 
00769     return app_iter, response[0], response[1]