comparison MoinMoin/support/werkzeug/test.py @ 4301:c689dfa55de1

Import werkzeug 0.3.1 package into MoinMoin.support (still need to insert CREDITS)
author Florian Krupicka <florian.krupicka@googlemail.com>
date Wed, 13 Aug 2008 21:05:42 +0200
parents
children 246ba4eecab2
comparison
equal deleted inserted replaced
4300:867da7983aba 4301:c689dfa55de1
1 # -*- coding: utf-8 -*-
2 """
3 werkzeug.test
4 ~~~~~~~~~~~~~
5
6 Quite often you want to unittest your application or just check the output
7 from an interactive python session. In theory that is pretty simple because
8 you can fake a WSGI environment and call the application with a dummy
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.
48 """
49 from time import time
50 from random import random
51 from urllib import urlencode
52 from cStringIO import StringIO
53 from mimetypes import guess_type
54 from werkzeug.wrappers import BaseResponse
55 from werkzeug.utils import create_environ, run_wsgi_app
56
57
58 def encode_multipart(values):
59 """Encode a dict of values (can either be strings or file descriptors)
60 into a multipart encoded string. The filename is taken from the `.name`
61 attribute of the file descriptor. Because StringIOs do not provide
62 this attribute it will generate a random filename in that case.
63
64 The return value is a tuple in the form (``boundary``, ``data``).
65
66 This method does not accept unicode strings!
67 """
68 boundary = '-----------=_Part_%s%s' (time(), random())
69 lines = []
70 for key, value in values.iteritems():
71 if isinstance(value, File):
72 lines.extend((
73 '--' + boundary,
74 'Content-Disposition: form-data; name="%s"; filename="%s"' %
75 (key, value.filename),
76 'Content-Type: ' + value.mimetype,
77 '',
78 value.read()
79 ))
80 else:
81 lines.extend((
82 '--' + boundary,
83 'Content-Disposition: form-data; name="%s"' % key,
84 '',
85 value
86 ))
87 lines.extend(('--' + boundary + '--', ''))
88 return boundary, '\r\n'.join(lines)
89
90
91 class File(object):
92 """Wraps a file descriptor or any other stream so that `encode_multipart`
93 can get the mimetype and filename from it.
94 """
95
96 def __init__(self, fd, filename=None, mimetype=None):
97 if isinstance(fd, basestring):
98 if filename is None:
99 filename = fd
100 fd = file(fd, 'rb')
101 try:
102 self.stream = StringIO(fd.read())
103 finally:
104 fd.close()
105 else:
106 self.stream = fd
107 if filename is None:
108 if not hasattr(fd, 'name'):
109 raise ValueError('no filename for provided')
110 filename = fd.name
111 if mimetype is None:
112 mimetype = guess_type(filename)
113 self.filename = fileanme
114 self.mimetype = mimetype or 'application/octet-stream'
115
116 def getattr(self, name):
117 return getattr(self.stream, name)
118
119 def __repr__(self):
120 return '<%s %r>' % (
121 self.__class__.__name__,
122 self.filename
123 )
124
125
126 class Client(object):
127 """This class allows to send requests to a wrapped application."""
128
129 def __init__(self, application, response_wrapper=None):
130 """The response wrapper can be a class or factory function that takes
131 three arguments: app_iter, status and headers. The default response
132 wrapper just returns a tuple.
133
134 Example::
135
136 class ClientResponse(BaseResponse):
137 ...
138
139 client = Client(MyApplication(), response_wrapper=ClientResponse)
140 """
141 self.application = application
142 if response_wrapper is None:
143 response_wrapper = lambda a, s, h: (a, s, h)
144 self.response_wrapper = response_wrapper
145
146 def open(self, path='/', base_url=None, query_string=None, method='GET',
147 data=None, input_stream=None, content_type=None,
148 content_length=0, errors_stream=None, multithread=False,
149 multiprocess=False, run_once=False, environ_overrides=None,
150 as_tuple=False):
151 """Takes the same arguments as the `create_environ` function from the
152 utility module with some additions.
153
154 The first parameter should be the path of the request which defaults to
155 '/'. The second one can either be a absolute path (in that case the url
156 host is localhost:80) or a full path to the request with scheme,
157 netloc port and the path to the script.
158
159 If the `path` contains a query string it will be used, even if the
160 `query_string` parameter was given. If it does not contain one
161 the `query_string` parameter is used as querystring. In that case
162 it can either be a dict, MultiDict or string.
163
164 The following options exist:
165
166 `method`
167 The request method. Defaults to `GET`
168
169 `input_stream`
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 """
209 if input_stream is None and data and method in ('PUT', 'POST'):
210 need_multipart = False
211 if isinstance(data, basestring):
212 assert content_type is not None, 'content type required'
213 else:
214 for key, value in data.iteritems():
215 if isinstance(value, basestring):
216 if isinstance(value, unicode):
217 data[key] = str(value)
218 continue
219 need_multipart = True
220 if isinstance(value, tuple):
221 data[key] = File(*value)
222 elif isinstance(value, dict):
223 data[key] = File(**value)
224 elif not isinstance(value, File):
225 data[key] = File(value)
226 if need_multipart:
227 boundary, data = encode_multipart(data)
228 if content_type is None:
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)
251 if as_tuple:
252 return environ, response
253 return response
254
255 def get(self, *args, **kw):
256 """Like open but method is enforced to GET"""
257 kw['method'] = 'GET'
258 return self.open(*args, **kw)
259
260 def post(self, *args, **kw):
261 """Like open but method is enforced to POST"""
262 kw['method'] = 'POST'
263 return self.open(*args, **kw)
264
265 def head(self, *args, **kw):
266 """Like open but method is enforced to HEAD"""
267 kw['method'] = 'HEAD'
268 return self.open(*args, **kw)
269
270 def put(self, *args, **kw):
271 """Like open but method is enforced to PUT"""
272 kw['method'] = 'PUT'
273 return self.open(*args, **kw)
274
275 def delete(self, *args, **kw):
276 """Like open but method is enforced to DELETE"""
277 kw['method'] = 'DELETE'
278 return self.open(*args, **kw)
279
280 def __repr__(self):
281 return '<%s %r>' % (
282 self.__class__.__name__,
283 self.application
284 )