~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/support/werkzeug/test.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mto: This revision was merged to the branch mainline in revision 18.
  • Revision ID: james.westby@ubuntu.com-20080622211713-inlv5k4eifxckelr
ImportĀ upstreamĀ versionĀ 1.7.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
"""
3
 
    werkzeug.test
4
 
    ~~~~~~~~~~~~~
5
 
 
6
 
    This module implements a client to WSGI applications for testing.
7
 
 
8
 
    :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
9
 
    :license: BSD, see LICENSE for more details.
10
 
"""
11
 
import sys
12
 
import urlparse
13
 
import mimetypes
14
 
from time import time
15
 
from random import random
16
 
from itertools import chain
17
 
from tempfile import TemporaryFile
18
 
from cStringIO import StringIO
19
 
from cookielib import CookieJar
20
 
from urllib2 import Request as U2Request
21
 
 
22
 
from werkzeug._internal import _empty_stream
23
 
from werkzeug.wrappers import BaseRequest
24
 
from werkzeug.utils import create_environ, run_wsgi_app, get_current_url, \
25
 
     url_encode, url_decode, FileStorage, get_host
26
 
from werkzeug.datastructures import FileMultiDict, MultiDict, \
27
 
     CombinedMultiDict, Headers
28
 
 
29
 
 
30
 
def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
31
 
                            boundary=None, charset='utf-8'):
32
 
    """Encode a dict of values (either strings or file descriptors or
33
 
    :class:`FileStorage` objects.) into a multipart encoded string stored
34
 
    in a file descriptor.
35
 
    """
36
 
    if boundary is None:
37
 
        boundary = '---------------WerkzeugFormPart_%s%s' % (time(), random())
38
 
    _closure = [StringIO(), 0, False]
39
 
 
40
 
    if use_tempfile:
41
 
        def write(string):
42
 
            stream, total_length, on_disk = _closure
43
 
            if on_disk:
44
 
                stream.write(string)
45
 
            else:
46
 
                length = len(string)
47
 
                if length + _closure[1] <= threshold:
48
 
                    stream.write(string)
49
 
                else:
50
 
                    new_stream = TemporaryFile('wb+')
51
 
                    new_stream.write(stream.getvalue())
52
 
                    new_stream.write(string)
53
 
                    _closure[0] = new_stream
54
 
                    _closure[2] = True
55
 
                _closure[1] = total_length + length
56
 
    else:
57
 
        write = _closure[0].write
58
 
 
59
 
    if not isinstance(values, MultiDict):
60
 
        values = MultiDict(values)
61
 
 
62
 
    for key, values in values.iterlists():
63
 
        for value in values:
64
 
            write('--%s\r\nContent-Disposition: form-data; name="%s"' %
65
 
                  (boundary, key))
66
 
            reader = getattr(value, 'read', None)
67
 
            if reader is not None:
68
 
                filename = getattr(value, 'filename',
69
 
                                   getattr(value, 'name', None))
70
 
                content_type = getattr(value, 'content_type', None)
71
 
                if content_type is None:
72
 
                    content_type = filename and \
73
 
                        mimetypes.guess_type(filename)[0] or \
74
 
                        'application/octet-stream'
75
 
                if filename is not None:
76
 
                    write('; filename="%s"\r\n' % filename)
77
 
                else:
78
 
                    write('\r\n')
79
 
                write('Content-Type: %s\r\n\r\n' % content_type)
80
 
                while 1:
81
 
                    chunk = reader(16384)
82
 
                    if not chunk:
83
 
                        break
84
 
                    write(chunk)
85
 
            else:
86
 
                if isinstance(value, unicode):
87
 
                    value = value.encode(charset)
88
 
                write('\r\n\r\n' + value)
89
 
            write('\r\n')
90
 
    write('--%s--\r\n' % boundary)
91
 
 
92
 
    length = int(_closure[0].tell())
93
 
    _closure[0].seek(0)
94
 
    return _closure[0], length, boundary
95
 
 
96
 
 
97
 
def encode_multipart(values, boundary=None, charset='utf-8'):
98
 
    """Like `stream_encode_multipart` but returns a tuple in the form
99
 
    (``boundary``, ``data``) where data is a bytestring.
100
 
    """
101
 
    stream, length, boundary = stream_encode_multipart(
102
 
        values, use_tempfile=False, boundary=boundary, charset=charset)
103
 
    return boundary, stream.read()
104
 
 
105
 
 
106
 
def File(fd, filename=None, mimetype=None):
107
 
    """Backwards compat."""
108
 
    from warnings import warn
109
 
    warn(DeprecationWarning('werkzeug.test.File is deprecated, use the '
110
 
                            'EnvironBuilder or FileStorage instead'))
111
 
    return FileStorage(fd, filename=filename, content_type=mimetype)
112
 
 
113
 
 
114
 
class _TestCookieHeaders(object):
115
 
    """A headers adapter for cookielib
116
 
    """
117
 
 
118
 
    def __init__(self, headers):
119
 
        self.headers = headers
120
 
 
121
 
    def getheaders(self, name):
122
 
        headers = []
123
 
        name = name.lower()
124
 
        for k, v in self.headers:
125
 
            if k.lower() == name:
126
 
                headers.append(v)
127
 
        return headers
128
 
 
129
 
 
130
 
class _TestCookieResponse(object):
131
 
    """Something that looks like a httplib.HTTPResponse, but is actually just an
132
 
    adapter for our test responses to make them available for cookielib.
133
 
    """
134
 
 
135
 
    def __init__(self, headers):
136
 
        self.headers = _TestCookieHeaders(headers)
137
 
 
138
 
    def info(self):
139
 
        return self.headers
140
 
 
141
 
 
142
 
class _TestCookieJar(CookieJar):
143
 
    """A cookielib.CookieJar modified to inject and read cookie headers from
144
 
    and to wsgi environments, and wsgi application responses.
145
 
    """
146
 
 
147
 
    def inject_wsgi(self, environ):
148
 
        """Inject the cookies as client headers into the server's wsgi
149
 
        environment.
150
 
        """
151
 
        cvals = []
152
 
        for cookie in self:
153
 
            cvals.append('%s=%s' % (cookie.name, cookie.value))
154
 
        if cvals:
155
 
            environ['HTTP_COOKIE'] = ','.join(cvals)
156
 
 
157
 
    def extract_wsgi(self, environ, headers):
158
 
        """Extract the server's set-cookie headers as cookies into the
159
 
        cookie jar.
160
 
        """
161
 
        self.extract_cookies(
162
 
            _TestCookieResponse(headers),
163
 
            U2Request(get_current_url(environ)),
164
 
        )
165
 
 
166
 
 
167
 
def _iter_data(data):
168
 
    """Iterates over a dict or multidict yielding all keys and values.
169
 
    This is used to iterate over the data passed to the
170
 
    :class:`EnvironBuilder`.
171
 
    """
172
 
    if isinstance(data, MultiDict):
173
 
        for key, values in data.iterlists():
174
 
            for value in values:
175
 
                yield key, value
176
 
    else:
177
 
        for item in data.iteritems():
178
 
            yield item
179
 
 
180
 
 
181
 
class EnvironBuilder(object):
182
 
    """This class can be used to conveniently create a WSGI environment
183
 
    for testing purposes.  It can be used to quickly create WSGI environments
184
 
    or request objects from arbitrary data.
185
 
 
186
 
    The signature of this class is also used in some other places as of
187
 
    Werkzeug 0.5 (:func:`create_environ`, :meth:`BaseResponse.from_values`,
188
 
    :meth:`Client.open`).  Because of this most of the functionality is
189
 
    available through the constructor alone.
190
 
 
191
 
    Files and regular form data can be manipulated independently of each
192
 
    other with the :attr:`form` and :attr:`files` attributes, but are
193
 
    passed with the same argument to the constructor: `data`.
194
 
 
195
 
    `data` can be any of these values:
196
 
 
197
 
    -   a `str`: If it's a string it is converted into a :attr:`input_stream`,
198
 
        the :attr:`content_length` is set and you have to provide a
199
 
        :attr:`content_type`.
200
 
    -   a `dict`: If it's a dict the keys have to be strings and the values
201
 
        and of the following objects:
202
 
 
203
 
        -   a :class:`file`-like object.  These are converted into
204
 
            :class:`FileStorage` objects automatically.
205
 
        -   a tuple.  The :meth:`~FileMultiDict.add_file` method is called
206
 
            with the tuple items as positional arguments.
207
 
 
208
 
    :param path: the path of the request.  In the WSGI environment this will
209
 
                 end up as `PATH_INFO`.  If the `query_string` is not defined
210
 
                 and there is a question mark in the `path` everything after
211
 
                 it is used as query string.
212
 
    :param base_url: the base URL is a URL that is used to extract the WSGI
213
 
                     URL scheme, host (server name + server port) and the
214
 
                     script root (`SCRIPT_NAME`).
215
 
    :param query_string: an optional string or dict with URL parameters.
216
 
    :param method: the HTTP method to use, defaults to `GET`.
217
 
    :param input_stream: an optional input stream.  Do not specify this and
218
 
                         `data`.  As soon as an input stream is set you can't
219
 
                         modify :attr:`args` and :attr:`files` unless you
220
 
                         set the :attr:`input_stream` to `None` again.
221
 
    :param content_type: The content type for the request.  As of 0.5 you
222
 
                         don't have to provide this when specifying files
223
 
                         and form data via `data`.
224
 
    :param content_length: The content length for the request.  You don't
225
 
                           have to specify this when providing data via
226
 
                           `data`.
227
 
    :param errors_stream: an optional error stream that is used for
228
 
                          `wsgi.errors`.  Defaults to :data:`stderr`.
229
 
    :param multithread: controls `wsgi.multithread`.  Defaults to `False`.
230
 
    :param multiprocess: controls `wsgi.multiprocess`.  Defaults to `False`.
231
 
    :param run_once: controls `wsgi.run_once`.  Defaults to `False`.
232
 
    :param headers: an optional list or :class:`Headers` object of headers.
233
 
    :param data: a string or dict of form data.  See explanation above.
234
 
    :param environ_base: an optional dict of environment defaults.
235
 
    :param environ_overrides: an optional dict of environment overrides.
236
 
    :param charset: the charset used to encode unicode data.
237
 
    """
238
 
 
239
 
    #: the server protocol to use.  defaults to HTTP/1.1
240
 
    server_protocol = 'HTTP/1.1'
241
 
 
242
 
    #: the wsgi version to use.  defaults to (1, 0)
243
 
    wsgi_version = (1, 0)
244
 
 
245
 
    #: the default request class for :meth:`get_request`
246
 
    request_class = BaseRequest
247
 
 
248
 
    def __init__(self, path='/', base_url=None, query_string=None,
249
 
                 method='GET', input_stream=None, content_type=None,
250
 
                 content_length=None, errors_stream=None, multithread=False,
251
 
                 multiprocess=False, run_once=False, headers=None, data=None,
252
 
                 environ_base=None, environ_overrides=None, charset='utf-8'):
253
 
        if query_string is None and '?' in path:
254
 
            path, query_string = path.split('?', 1)
255
 
        self.charset = charset
256
 
        self.path = path
257
 
        self.base_url = base_url
258
 
        if isinstance(query_string, basestring):
259
 
            self.query_string = query_string
260
 
        else:
261
 
            if query_string is None:
262
 
                query_string = MultiDict()
263
 
            elif not isinstance(query_string, MultiDict):
264
 
                query_string = MultiDict(query_string)
265
 
            self.args = query_string
266
 
        self.method = method
267
 
        if headers is None:
268
 
            headers = Headers()
269
 
        elif not isinstance(headers, Headers):
270
 
            headers = Headers(headers)
271
 
        self.headers = headers
272
 
        self.content_type = content_type
273
 
        if errors_stream is None:
274
 
            errors_stream = sys.stderr
275
 
        self.errors_stream = errors_stream
276
 
        self.multithread = multithread
277
 
        self.multiprocess = multiprocess
278
 
        self.run_once = run_once
279
 
        self.environ_base = environ_base
280
 
        self.environ_overrides = environ_overrides
281
 
        self.input_stream = input_stream
282
 
        self.content_length = content_length
283
 
        self.closed = False
284
 
 
285
 
        if data:
286
 
            if input_stream is not None:
287
 
                raise TypeError('can\'t provide input stream and data')
288
 
            if isinstance(data, basestring):
289
 
                self.input_stream = StringIO(data)
290
 
                if self.content_length is None:
291
 
                    self.content_length = len(data)
292
 
            else:
293
 
                for key, value in _iter_data(data):
294
 
                    if isinstance(value, (tuple, dict)) or \
295
 
                       hasattr(value, 'read'):
296
 
                        self._add_file_from_data(key, value)
297
 
                    else:
298
 
                        self.form[key] = value
299
 
 
300
 
    def _add_file_from_data(self, key, value):
301
 
        """Called in the EnvironBuilder to add files from the data dict."""
302
 
        if isinstance(value, tuple):
303
 
            self.files.add_file(key, *value)
304
 
        elif isinstance(value, dict):
305
 
            from warnings import warn
306
 
            warn(DeprecationWarning('it\'s no longer possible to pass dicts '
307
 
                                    'as `data`.  Use tuples or FileStorage '
308
 
                                    'objects intead'), stacklevel=2)
309
 
            args = v
310
 
            value = dict(value)
311
 
            mimetype = value.pop('mimetype', None)
312
 
            if mimetype is not None:
313
 
                value['content_type'] = mimetype
314
 
            self.files.add_file(key, **value)
315
 
        else:
316
 
            self.files.add_file(key, value)
317
 
 
318
 
    def _get_base_url(self):
319
 
        return urlparse.urlunsplit((self.url_scheme, self.host,
320
 
                                    self.script_root, '', '')).rstrip('/') + '/'
321
 
 
322
 
    def _set_base_url(self, value):
323
 
        if value is None:
324
 
            scheme = 'http'
325
 
            netloc = 'localhost'
326
 
            scheme = 'http'
327
 
            script_root = ''
328
 
        else:
329
 
            scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(value)
330
 
            if qs or anchor:
331
 
                raise ValueError('base url must not contain a query string '
332
 
                                 'or fragment')
333
 
        self.script_root = script_root.rstrip('/')
334
 
        self.host = netloc
335
 
        self.url_scheme = scheme
336
 
 
337
 
    base_url = property(_get_base_url, _set_base_url, doc='''
338
 
        The base URL is a URL that is used to extract the WSGI
339
 
        URL scheme, host (server name + server port) and the
340
 
        script root (`SCRIPT_NAME`).''')
341
 
    del _get_base_url, _set_base_url
342
 
 
343
 
    def _get_content_type(self):
344
 
        ct = self.headers.get('Content-Type')
345
 
        if ct is None and not self._input_stream:
346
 
            if self.method in ('POST', 'PUT'):
347
 
                if self._files:
348
 
                    return 'multipart/form-data'
349
 
                return 'application/x-www-form-urlencoded'
350
 
            return None
351
 
        return ct
352
 
 
353
 
    def _set_content_type(self, value):
354
 
        if value is None:
355
 
            self.headers.pop('Content-Type', None)
356
 
        else:
357
 
            self.headers['Content-Type'] = value
358
 
 
359
 
    content_type = property(_get_content_type, _set_content_type, doc='''
360
 
        The content type for the request.  Reflected from and to the
361
 
        :attr:`headers`.  Do not set if you set :attr:`files` or
362
 
        :attr:`form` for auto detection.''')
363
 
    del _get_content_type, _set_content_type
364
 
 
365
 
    def _get_content_length(self):
366
 
        return self.headers.get('Content-Length', type=int)
367
 
 
368
 
    def _set_content_length(self, value):
369
 
        if value is None:
370
 
            self.headers.pop('Content-Length', None)
371
 
        else:
372
 
            self.headers['Content-Length'] = str(value)
373
 
 
374
 
    content_length = property(_get_content_length, _set_content_length, doc='''
375
 
        The content length as integer.  Reflected from and to the
376
 
        :attr:`headers`.  Do not set if you set :attr:`files` or
377
 
        :attr:`form` for auto detection.''')
378
 
    del _get_content_length, _set_content_length
379
 
 
380
 
    def form_property(name, storage, doc):
381
 
        key = '_' + name
382
 
        def getter(self):
383
 
            if self._input_stream is not None:
384
 
                raise AttributeError('an input stream is defined')
385
 
            rv = getattr(self, key)
386
 
            if rv is None:
387
 
                rv = storage()
388
 
                setattr(self, key, rv)
389
 
            return rv
390
 
        def setter(self, value):
391
 
            self._input_stream = None
392
 
            setattr(self, key, value)
393
 
        return property(getter, setter, doc)
394
 
 
395
 
    form = form_property('form', MultiDict, doc='''
396
 
        A :class:`MultiDict` of form values.''')
397
 
    files = form_property('files', FileMultiDict, doc='''
398
 
        A :class:`FileMultiDict` of uploaded files.  You can use the
399
 
        :meth:`~FileMultiDict.add_file` method to add new files to the
400
 
        dict.''')
401
 
    del form_property
402
 
 
403
 
    def _get_input_stream(self):
404
 
        return self._input_stream
405
 
 
406
 
    def _set_input_stream(self, value):
407
 
        self._input_stream = value
408
 
        self._form = self._files = None
409
 
 
410
 
    input_stream = property(_get_input_stream, _set_input_stream, doc='''
411
 
        An optional input stream.  If you set this it will clear
412
 
        :attr:`form` and :attr:`files`.''')
413
 
    del _get_input_stream, _set_input_stream
414
 
 
415
 
    def _get_query_string(self):
416
 
        if self._query_string is None:
417
 
            if self._args is not None:
418
 
                return url_encode(self._args, charset=self.charset)
419
 
            return ''
420
 
        return self._query_string
421
 
 
422
 
    def _set_query_string(self, value):
423
 
        self._query_string = value
424
 
        self._args = None
425
 
 
426
 
    query_string = property(_get_query_string, _set_query_string, doc='''
427
 
        The query string.  If you set this to a string :attr:`args` will
428
 
        no longer be available.''')
429
 
    del _get_query_string, _set_query_string
430
 
 
431
 
    def _get_args(self):
432
 
        if self._query_string is not None:
433
 
            raise AttributeError('a query string is defined')
434
 
        if self._args is None:
435
 
            self._args = MultiDict()
436
 
        return self._args
437
 
 
438
 
    def _set_args(self, value):
439
 
        self._query_string = None
440
 
        self._args = value
441
 
 
442
 
    args = property(_get_args, _set_args, doc='''
443
 
        The URL arguments as :class:`MultiDict`.''')
444
 
    del _get_args, _set_args
445
 
 
446
 
    @property
447
 
    def server_name(self):
448
 
        """The server name (read-only, use :attr:`host` to set)"""
449
 
        return self.host.split(':', 1)[0]
450
 
 
451
 
    @property
452
 
    def server_port(self):
453
 
        """The server port as integer (read-only, use :attr:`host` to set)"""
454
 
        pieces = self.host.split(':', 1)
455
 
        if len(pieces) == 2 and pieces[1].isdigit():
456
 
            return int(pieces[1])
457
 
        elif self.url_scheme == 'https':
458
 
            return 443
459
 
        return 80
460
 
 
461
 
    def __del__(self):
462
 
        self.close()
463
 
 
464
 
    def close(self):
465
 
        """Closes all files.  If you put real :class:`file` objects into the
466
 
        :attr:`files` dict you can call this method to automatically close
467
 
        them all in one go.
468
 
        """
469
 
        if self.closed:
470
 
            return
471
 
        try:
472
 
            files = self.files.itervalues()
473
 
        except AttributeError:
474
 
            files = ()
475
 
        for f in files:
476
 
            try:
477
 
                f.close()
478
 
            except Exception, e:
479
 
                pass
480
 
        self.closed = True
481
 
 
482
 
    def get_environ(self):
483
 
        """Return the built environ."""
484
 
        input_stream = self.input_stream
485
 
        content_length = self.content_length
486
 
        content_type = self.content_type
487
 
 
488
 
        if input_stream is not None:
489
 
            start_pos = input_stream.tell()
490
 
            input_stream.seek(0, 2)
491
 
            end_pos = input_stream.tell()
492
 
            input_stream.seek(start_pos)
493
 
            content_length = end_pos - start_pos
494
 
        elif content_type == 'multipart/form-data':
495
 
            values = CombinedMultiDict([self.form, self.files])
496
 
            input_stream, content_length, boundary = \
497
 
                stream_encode_multipart(values, charset=self.charset)
498
 
            content_type += '; boundary="%s"' % boundary
499
 
        elif content_type == 'application/x-www-form-urlencoded':
500
 
            values = url_encode(self.form, charset=self.charset)
501
 
            content_length = len(values)
502
 
            input_stream = StringIO(values)
503
 
        else:
504
 
            input_stream = _empty_stream
505
 
 
506
 
        result = {}
507
 
        if self.environ_base:
508
 
            result.update(self.environ_base)
509
 
 
510
 
        def _encode(x):
511
 
            if isinstance(x, unicode):
512
 
                return x.encode(self.charset)
513
 
            return x
514
 
 
515
 
        result.update({
516
 
            'REQUEST_METHOD':       self.method,
517
 
            'SCRIPT_NAME':          _encode(self.script_root),
518
 
            'PATH_INFO':            _encode(self.path),
519
 
            'QUERY_STRING':         self.query_string,
520
 
            'SERVER_NAME':          self.server_name,
521
 
            'SERVER_PORT':          str(self.server_port),
522
 
            'HTTP_HOST':            self.host,
523
 
            'SERVER_PROTOCOL':      self.server_protocol,
524
 
            'CONTENT_TYPE':         content_type or '',
525
 
            'CONTENT_LENGTH':       str(content_length or '0'),
526
 
            'wsgi.version':         self.wsgi_version,
527
 
            'wsgi.url_scheme':      self.url_scheme,
528
 
            'wsgi.input':           input_stream,
529
 
            'wsgi.errors':          self.errors_stream,
530
 
            'wsgi.multithread':     self.multithread,
531
 
            'wsgi.multiprocess':    self.multiprocess,
532
 
            'wsgi.run_once':        self.run_once
533
 
        })
534
 
        for key, value in self.headers.to_list(self.charset):
535
 
            result['HTTP_%s' % key.upper().replace('-', '_')] = value
536
 
        if self.environ_overrides:
537
 
            result.update(self.environ_overrides)
538
 
        return result
539
 
 
540
 
    def get_request(self, cls=None):
541
 
        """Returns a request with the data.  If the request class is not
542
 
        specified :attr:`request_class` is used.
543
 
 
544
 
        :param cls: The request wrapper to use.
545
 
        """
546
 
        if cls is None:
547
 
            cls = self.request_class
548
 
        return cls(self.get_environ())
549
 
 
550
 
 
551
 
class Client(object):
552
 
    """This class allows to send requests to a wrapped application.
553
 
 
554
 
    The response wrapper can be a class or factory function that takes
555
 
    three arguments: app_iter, status and headers.  The default response
556
 
    wrapper just returns a tuple.
557
 
 
558
 
    Example::
559
 
 
560
 
        class ClientResponse(BaseResponse):
561
 
            ...
562
 
 
563
 
        client = Client(MyApplication(), response_wrapper=ClientResponse)
564
 
 
565
 
    The use_cookies parameter indicates whether cookies should be stored and
566
 
    sent for subsequent requests. This is True by default, but passing False
567
 
    will disable this behaviour.
568
 
 
569
 
    .. versionadded:: 0.5
570
 
       `use_cookies` is new in this version.  Older versions did not provide
571
 
       builtin cookie support.
572
 
    """
573
 
 
574
 
    def __init__(self, application, response_wrapper=None, use_cookies=True):
575
 
        self.application = application
576
 
        if response_wrapper is None:
577
 
            response_wrapper = lambda a, s, h: (a, s, h)
578
 
        self.response_wrapper = response_wrapper
579
 
        if use_cookies:
580
 
            self.cookie_jar = _TestCookieJar()
581
 
        else:
582
 
            self.cookie_jar = None
583
 
 
584
 
    def open(self, *args, **kwargs):
585
 
        """Takes the same arguments as the :class:`EnvironBuilder` class with
586
 
        some additions:  You can provide a :class:`EnvironBuilder` or a WSGI
587
 
        environment as only argument instead of the :class:`EnvironBuilder`
588
 
        arguments and two optional keyword arguments (`as_tuple`, `buffered`)
589
 
        that change the type of the return value or the way the application is
590
 
        executed.
591
 
 
592
 
        .. versionchanged:: 0.5
593
 
           If a dict is provided as file in the dict for the `data` parameter
594
 
           the content type has to be called `content_type` now instead of
595
 
           `mimetype`.  This change was made for consistency with
596
 
           :class:`werkzeug.FileWrapper`.
597
 
 
598
 
            The `follow_redirects` parameter was added to :func:`open`.
599
 
 
600
 
        Additional parameters:
601
 
 
602
 
        :param as_tuple: Returns a tuple in the form ``(environ, result)``
603
 
        :param buffered: Set this to true to buffer the application run.
604
 
                         This will automatically close the application for
605
 
                         you as well.
606
 
        :param follow_redirects: Set this to True if the `Client` should
607
 
                                 follow HTTP redirects.
608
 
        """
609
 
        as_tuple = kwargs.pop('as_tuple', False)
610
 
        buffered = kwargs.pop('buffered', False)
611
 
        follow_redirects = kwargs.pop('follow_redirects', False)
612
 
        environ = None
613
 
        if not kwargs and len(args) == 1:
614
 
            if isinstance(args[0], EnvironBuilder):
615
 
                environ = args[0].get_environ()
616
 
            elif isinstance(args[0], dict):
617
 
                environ = args[0]
618
 
        if environ is None:
619
 
            builder = EnvironBuilder(*args, **kwargs)
620
 
            try:
621
 
                environ = builder.get_environ()
622
 
            finally:
623
 
                builder.close()
624
 
 
625
 
        if self.cookie_jar is not None:
626
 
            self.cookie_jar.inject_wsgi(environ)
627
 
        rv = run_wsgi_app(self.application, environ, buffered=buffered)
628
 
        if self.cookie_jar is not None:
629
 
            self.cookie_jar.extract_wsgi(environ, rv[2])
630
 
 
631
 
        # handle redirects
632
 
        redirect_chain = []
633
 
        status_code = int(rv[1].split(None, 1)[0])
634
 
        while status_code in (301, 302, 303, 305, 307) and follow_redirects:
635
 
            redirect = dict(rv[2])['Location']
636
 
            host = get_host(create_environ('/', redirect))
637
 
            if get_host(environ).split(':', 1)[0] != host:
638
 
                raise RuntimeError('%r does not support redirect to '
639
 
                                   'external targets' % self.__class__)
640
 
 
641
 
            scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect)
642
 
            redirect_chain.append((redirect, status_code))
643
 
 
644
 
            kwargs.update({
645
 
                'base_url':         urlparse.urlunsplit((scheme, host,
646
 
                                    script_root, '', '')).rstrip('/') + '/',
647
 
                'query_string':     qs,
648
 
                'as_tuple':         as_tuple,
649
 
                'buffered':         buffered,
650
 
                'follow_redirects': False
651
 
            })
652
 
            rv = self.open(*args, **kwargs)
653
 
            status_code = int(rv[1].split(None, 1)[0])
654
 
 
655
 
            # Prevent loops
656
 
            if redirect_chain[-1] in redirect_chain[0:-1]:
657
 
                break
658
 
 
659
 
        response = self.response_wrapper(*rv)
660
 
        if as_tuple:
661
 
            return environ, response
662
 
        return response
663
 
 
664
 
    def get(self, *args, **kw):
665
 
        """Like open but method is enforced to GET."""
666
 
        kw['method'] = 'GET'
667
 
        return self.open(*args, **kw)
668
 
 
669
 
    def post(self, *args, **kw):
670
 
        """Like open but method is enforced to POST."""
671
 
        kw['method'] = 'POST'
672
 
        return self.open(*args, **kw)
673
 
 
674
 
    def head(self, *args, **kw):
675
 
        """Like open but method is enforced to HEAD."""
676
 
        kw['method'] = 'HEAD'
677
 
        return self.open(*args, **kw)
678
 
 
679
 
    def put(self, *args, **kw):
680
 
        """Like open but method is enforced to PUT."""
681
 
        kw['method'] = 'PUT'
682
 
        return self.open(*args, **kw)
683
 
 
684
 
    def delete(self, *args, **kw):
685
 
        """Like open but method is enforced to DELETE."""
686
 
        kw['method'] = 'DELETE'
687
 
        return self.open(*args, **kw)
688
 
 
689
 
    def __repr__(self):
690
 
        return '<%s %r>' % (
691
 
            self.__class__.__name__,
692
 
            self.application
693
 
        )
694
 
 
695
 
 
696
 
def create_environ(*args, **kwargs):
697
 
    """Create a new WSGI environ dict based on the values passed.  The first
698
 
    parameter should be the path of the request which defaults to '/'.  The
699
 
    second one can either be an absolute path (in that case the host is
700
 
    localhost:80) or a full path to the request with scheme, netloc port and
701
 
    the path to the script.
702
 
 
703
 
    This accepts the same arguments as the :class:`EnvironBuilder`
704
 
    constructor.
705
 
 
706
 
    .. versionchanged:: 0.5
707
 
       This function is now a thin wrapper over :class:`EnvironBuilder` which
708
 
       was added in 0.5.  The `headers`, `environ_base`, `environ_overrides`
709
 
       and `charset` parameters were added.
710
 
    """
711
 
    builder = EnvironBuilder(*args, **kwargs)
712
 
    try:
713
 
        return builder.get_environ()
714
 
    finally:
715
 
        builder.close()
716
 
 
717
 
 
718
 
def run_wsgi_app(app, environ, buffered=False):
719
 
    """Return a tuple in the form (app_iter, status, headers) of the
720
 
    application output.  This works best if you pass it an application that
721
 
    returns an iterator all the time.
722
 
 
723
 
    Sometimes applications may use the `write()` callable returned
724
 
    by the `start_response` function.  This tries to resolve such edge
725
 
    cases automatically.  But if you don't get the expected output you
726
 
    should set `buffered` to `True` which enforces buffering.
727
 
 
728
 
    If passed an invalid WSGI application the behavior of this function is
729
 
    undefined.  Never pass non-conforming WSGI applications to this function.
730
 
 
731
 
    :param app: the application to execute.
732
 
    :param buffered: set to `True` to enforce buffering.
733
 
    :return: tuple in the form ``(app_iter, status, headers)``
734
 
    """
735
 
    response = []
736
 
    buffer = []
737
 
 
738
 
    def start_response(status, headers, exc_info=None):
739
 
        if exc_info is not None:
740
 
            raise exc_info[0], exc_info[1], exc_info[2]
741
 
        response[:] = [status, headers]
742
 
        return buffer.append
743
 
 
744
 
    app_iter = app(environ, start_response)
745
 
 
746
 
    # when buffering we emit the close call early and conver the
747
 
    # application iterator into a regular list
748
 
    if buffered:
749
 
        close_func = getattr(app_iter, 'close', None)
750
 
        try:
751
 
            app_iter = list(app_iter)
752
 
        finally:
753
 
            if close_func is not None:
754
 
                close_func()
755
 
 
756
 
    # otherwise we iterate the application iter until we have
757
 
    # a response, chain the already received data with the already
758
 
    # collected data and wrap it in a new `ClosingIterator` if
759
 
    # we have a close callable.
760
 
    else:
761
 
        while not response:
762
 
            buffer.append(app_iter.next())
763
 
        if buffer:
764
 
            app_iter = chain(buffer, app_iter)
765
 
            close_func = getattr(app_iter, 'close', None)
766
 
            if close_func is not None:
767
 
                app_iter = ClosingIterator(app_iter, close_func)
768
 
 
769
 
    return app_iter, response[0], response[1]