comparison MoinMoin/support/werkzeug/test.py @ 4609:246ba4eecab2

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