1
# -*- coding: utf-8 -*-
6
This module implements a client to WSGI applications for testing.
8
:copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details.
9
:license: BSD, see LICENSE for more details.
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
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
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
37
boundary = '---------------WerkzeugFormPart_%s%s' % (time(), random())
38
_closure = [StringIO(), 0, False]
42
stream, total_length, on_disk = _closure
47
if length + _closure[1] <= threshold:
50
new_stream = TemporaryFile('wb+')
51
new_stream.write(stream.getvalue())
52
new_stream.write(string)
53
_closure[0] = new_stream
55
_closure[1] = total_length + length
57
write = _closure[0].write
59
if not isinstance(values, MultiDict):
60
values = MultiDict(values)
62
for key, values in values.iterlists():
64
write('--%s\r\nContent-Disposition: form-data; name="%s"' %
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)
79
write('Content-Type: %s\r\n\r\n' % content_type)
86
if isinstance(value, unicode):
87
value = value.encode(charset)
88
write('\r\n\r\n' + value)
90
write('--%s--\r\n' % boundary)
92
length = int(_closure[0].tell())
94
return _closure[0], length, boundary
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.
101
stream, length, boundary = stream_encode_multipart(
102
values, use_tempfile=False, boundary=boundary, charset=charset)
103
return boundary, stream.read()
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)
114
class _TestCookieHeaders(object):
115
"""A headers adapter for cookielib
118
def __init__(self, headers):
119
self.headers = headers
121
def getheaders(self, name):
124
for k, v in self.headers:
125
if k.lower() == name:
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.
135
def __init__(self, headers):
136
self.headers = _TestCookieHeaders(headers)
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.
147
def inject_wsgi(self, environ):
148
"""Inject the cookies as client headers into the server's wsgi
153
cvals.append('%s=%s' % (cookie.name, cookie.value))
155
environ['HTTP_COOKIE'] = ','.join(cvals)
157
def extract_wsgi(self, environ, headers):
158
"""Extract the server's set-cookie headers as cookies into the
161
self.extract_cookies(
162
_TestCookieResponse(headers),
163
U2Request(get_current_url(environ)),
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`.
172
if isinstance(data, MultiDict):
173
for key, values in data.iterlists():
177
for item in data.iteritems():
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.
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.
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`.
195
`data` can be any of these values:
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:
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.
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
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.
239
#: the server protocol to use. defaults to HTTP/1.1
240
server_protocol = 'HTTP/1.1'
242
#: the wsgi version to use. defaults to (1, 0)
243
wsgi_version = (1, 0)
245
#: the default request class for :meth:`get_request`
246
request_class = BaseRequest
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
257
self.base_url = base_url
258
if isinstance(query_string, basestring):
259
self.query_string = query_string
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
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
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)
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)
298
self.form[key] = value
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)
311
mimetype = value.pop('mimetype', None)
312
if mimetype is not None:
313
value['content_type'] = mimetype
314
self.files.add_file(key, **value)
316
self.files.add_file(key, value)
318
def _get_base_url(self):
319
return urlparse.urlunsplit((self.url_scheme, self.host,
320
self.script_root, '', '')).rstrip('/') + '/'
322
def _set_base_url(self, value):
329
scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(value)
331
raise ValueError('base url must not contain a query string '
333
self.script_root = script_root.rstrip('/')
335
self.url_scheme = scheme
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
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'):
348
return 'multipart/form-data'
349
return 'application/x-www-form-urlencoded'
353
def _set_content_type(self, value):
355
self.headers.pop('Content-Type', None)
357
self.headers['Content-Type'] = value
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
365
def _get_content_length(self):
366
return self.headers.get('Content-Length', type=int)
368
def _set_content_length(self, value):
370
self.headers.pop('Content-Length', None)
372
self.headers['Content-Length'] = str(value)
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
380
def form_property(name, storage, doc):
383
if self._input_stream is not None:
384
raise AttributeError('an input stream is defined')
385
rv = getattr(self, key)
388
setattr(self, key, rv)
390
def setter(self, value):
391
self._input_stream = None
392
setattr(self, key, value)
393
return property(getter, setter, doc)
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
403
def _get_input_stream(self):
404
return self._input_stream
406
def _set_input_stream(self, value):
407
self._input_stream = value
408
self._form = self._files = None
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
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)
420
return self._query_string
422
def _set_query_string(self, value):
423
self._query_string = value
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
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()
438
def _set_args(self, value):
439
self._query_string = None
442
args = property(_get_args, _set_args, doc='''
443
The URL arguments as :class:`MultiDict`.''')
444
del _get_args, _set_args
447
def server_name(self):
448
"""The server name (read-only, use :attr:`host` to set)"""
449
return self.host.split(':', 1)[0]
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':
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
472
files = self.files.itervalues()
473
except AttributeError:
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
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)
504
input_stream = _empty_stream
507
if self.environ_base:
508
result.update(self.environ_base)
511
if isinstance(x, unicode):
512
return x.encode(self.charset)
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
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)
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.
544
:param cls: The request wrapper to use.
547
cls = self.request_class
548
return cls(self.get_environ())
551
class Client(object):
552
"""This class allows to send requests to a wrapped application.
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.
560
class ClientResponse(BaseResponse):
563
client = Client(MyApplication(), response_wrapper=ClientResponse)
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.
569
.. versionadded:: 0.5
570
`use_cookies` is new in this version. Older versions did not provide
571
builtin cookie support.
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
580
self.cookie_jar = _TestCookieJar()
582
self.cookie_jar = None
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
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`.
598
The `follow_redirects` parameter was added to :func:`open`.
600
Additional parameters:
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
606
:param follow_redirects: Set this to True if the `Client` should
607
follow HTTP redirects.
609
as_tuple = kwargs.pop('as_tuple', False)
610
buffered = kwargs.pop('buffered', False)
611
follow_redirects = kwargs.pop('follow_redirects', False)
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):
619
builder = EnvironBuilder(*args, **kwargs)
621
environ = builder.get_environ()
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])
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__)
641
scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect)
642
redirect_chain.append((redirect, status_code))
645
'base_url': urlparse.urlunsplit((scheme, host,
646
script_root, '', '')).rstrip('/') + '/',
648
'as_tuple': as_tuple,
649
'buffered': buffered,
650
'follow_redirects': False
652
rv = self.open(*args, **kwargs)
653
status_code = int(rv[1].split(None, 1)[0])
656
if redirect_chain[-1] in redirect_chain[0:-1]:
659
response = self.response_wrapper(*rv)
661
return environ, response
664
def get(self, *args, **kw):
665
"""Like open but method is enforced to GET."""
667
return self.open(*args, **kw)
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)
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)
679
def put(self, *args, **kw):
680
"""Like open but method is enforced to PUT."""
682
return self.open(*args, **kw)
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)
691
self.__class__.__name__,
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.
703
This accepts the same arguments as the :class:`EnvironBuilder`
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.
711
builder = EnvironBuilder(*args, **kwargs)
713
return builder.get_environ()
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.
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.
728
If passed an invalid WSGI application the behavior of this function is
729
undefined. Never pass non-conforming WSGI applications to this function.
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)``
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]
744
app_iter = app(environ, start_response)
746
# when buffering we emit the close call early and conver the
747
# application iterator into a regular list
749
close_func = getattr(app_iter, 'close', None)
751
app_iter = list(app_iter)
753
if close_func is not None:
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.
762
buffer.append(app_iter.next())
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)
769
return app_iter, response[0], response[1]