comparison MoinMoin/support/werkzeug/test.py @ 5801:8de563c487be

upgrade werkzeug to 0.8.1, document current bundled version and current minimum requirement (0.6, for py 2.7 compatibility)
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Thu, 01 Dec 2011 01:34:45 +0100
parents 7cb92118a93e
children 9f12f41504fc
comparison
equal deleted inserted replaced
5800:4ab3c578e44b 5801:8de563c487be
3 werkzeug.test 3 werkzeug.test
4 ~~~~~~~~~~~~~ 4 ~~~~~~~~~~~~~
5 5
6 This module implements a client to WSGI applications for testing. 6 This module implements a client to WSGI applications for testing.
7 7
8 :copyright: (c) 2009 by the Werkzeug Team, see AUTHORS for more details. 8 :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
9 :license: BSD, see LICENSE for more details. 9 :license: BSD, see LICENSE for more details.
10 """ 10 """
11 import sys 11 import sys
12 import urlparse 12 import urlparse
13 import mimetypes 13 import mimetypes
17 from tempfile import TemporaryFile 17 from tempfile import TemporaryFile
18 from cStringIO import StringIO 18 from cStringIO import StringIO
19 from cookielib import CookieJar 19 from cookielib import CookieJar
20 from urllib2 import Request as U2Request 20 from urllib2 import Request as U2Request
21 21
22 from werkzeug._internal import _empty_stream 22 from werkzeug._internal import _empty_stream, _get_environ
23 from werkzeug.wrappers import BaseRequest 23 from werkzeug.wrappers import BaseRequest
24 from werkzeug.utils import create_environ, run_wsgi_app, get_current_url, \ 24 from werkzeug.urls import url_encode, url_fix, iri_to_uri, _unquote
25 url_encode, url_decode, FileStorage, get_host 25 from werkzeug.wsgi import get_host, get_current_url, ClosingIterator
26 from werkzeug.utils import dump_cookie
26 from werkzeug.datastructures import FileMultiDict, MultiDict, \ 27 from werkzeug.datastructures import FileMultiDict, MultiDict, \
27 CombinedMultiDict, Headers 28 CombinedMultiDict, Headers, FileStorage
28 29
29 30
30 def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500, 31 def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
31 boundary=None, charset='utf-8'): 32 boundary=None, charset='utf-8'):
32 """Encode a dict of values (either strings or file descriptors or 33 """Encode a dict of values (either strings or file descriptors or
83 break 84 break
84 write(chunk) 85 write(chunk)
85 else: 86 else:
86 if isinstance(value, unicode): 87 if isinstance(value, unicode):
87 value = value.encode(charset) 88 value = value.encode(charset)
88 write('\r\n\r\n' + value) 89 write('\r\n\r\n' + str(value))
89 write('\r\n') 90 write('\r\n')
90 write('--%s--\r\n' % boundary) 91 write('--%s--\r\n' % boundary)
91 92
92 length = int(_closure[0].tell()) 93 length = int(_closure[0].tell())
93 _closure[0].seek(0) 94 _closure[0].seek(0)
150 """ 151 """
151 cvals = [] 152 cvals = []
152 for cookie in self: 153 for cookie in self:
153 cvals.append('%s=%s' % (cookie.name, cookie.value)) 154 cvals.append('%s=%s' % (cookie.name, cookie.value))
154 if cvals: 155 if cvals:
155 environ['HTTP_COOKIE'] = ','.join(cvals) 156 environ['HTTP_COOKIE'] = '; '.join(cvals)
156 157
157 def extract_wsgi(self, environ, headers): 158 def extract_wsgi(self, environ, headers):
158 """Extract the server's set-cookie headers as cookies into the 159 """Extract the server's set-cookie headers as cookies into the
159 cookie jar. 160 cookie jar.
160 """ 161 """
172 if isinstance(data, MultiDict): 173 if isinstance(data, MultiDict):
173 for key, values in data.iterlists(): 174 for key, values in data.iterlists():
174 for value in values: 175 for value in values:
175 yield key, value 176 yield key, value
176 else: 177 else:
177 for item in data.iteritems(): 178 for key, values in data.iteritems():
178 yield item 179 if isinstance(values, list):
180 for value in values:
181 yield key, value
182 else:
183 yield key, values
179 184
180 185
181 class EnvironBuilder(object): 186 class EnvironBuilder(object):
182 """This class can be used to conveniently create a WSGI environment 187 """This class can be used to conveniently create a WSGI environment
183 for testing purposes. It can be used to quickly create WSGI environments 188 for testing purposes. It can be used to quickly create WSGI environments
196 201
197 - a `str`: If it's a string it is converted into a :attr:`input_stream`, 202 - 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 203 the :attr:`content_length` is set and you have to provide a
199 :attr:`content_type`. 204 :attr:`content_type`.
200 - a `dict`: If it's a dict the keys have to be strings and the values 205 - a `dict`: If it's a dict the keys have to be strings and the values
201 and of the following objects: 206 any of the following objects:
202 207
203 - a :class:`file`-like object. These are converted into 208 - a :class:`file`-like object. These are converted into
204 :class:`FileStorage` objects automatically. 209 :class:`FileStorage` objects automatically.
205 - a tuple. The :meth:`~FileMultiDict.add_file` method is called 210 - a tuple. The :meth:`~FileMultiDict.add_file` method is called
206 with the tuple items as positional arguments. 211 with the tuple items as positional arguments.
212
213 .. versionadded:: 0.6
214 `path` and `base_url` can now be unicode strings that are encoded using
215 the :func:`iri_to_uri` function.
207 216
208 :param path: the path of the request. In the WSGI environment this will 217 :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 218 end up as `PATH_INFO`. If the `query_string` is not defined
210 and there is a question mark in the `path` everything after 219 and there is a question mark in the `path` everything after
211 it is used as query string. 220 it is used as query string.
251 multiprocess=False, run_once=False, headers=None, data=None, 260 multiprocess=False, run_once=False, headers=None, data=None,
252 environ_base=None, environ_overrides=None, charset='utf-8'): 261 environ_base=None, environ_overrides=None, charset='utf-8'):
253 if query_string is None and '?' in path: 262 if query_string is None and '?' in path:
254 path, query_string = path.split('?', 1) 263 path, query_string = path.split('?', 1)
255 self.charset = charset 264 self.charset = charset
265 if isinstance(path, unicode):
266 path = iri_to_uri(path, charset)
256 self.path = path 267 self.path = path
268 if base_url is not None:
269 if isinstance(base_url, unicode):
270 base_url = iri_to_uri(base_url, charset)
271 else:
272 base_url = url_fix(base_url, charset)
257 self.base_url = base_url 273 self.base_url = base_url
258 if isinstance(query_string, basestring): 274 if isinstance(query_string, basestring):
259 self.query_string = query_string 275 self.query_string = query_string
260 else: 276 else:
261 if query_string is None: 277 if query_string is None:
293 for key, value in _iter_data(data): 309 for key, value in _iter_data(data):
294 if isinstance(value, (tuple, dict)) or \ 310 if isinstance(value, (tuple, dict)) or \
295 hasattr(value, 'read'): 311 hasattr(value, 'read'):
296 self._add_file_from_data(key, value) 312 self._add_file_from_data(key, value)
297 else: 313 else:
298 self.form[key] = value 314 self.form.setlistdefault(key).append(value)
299 315
300 def _add_file_from_data(self, key, value): 316 def _add_file_from_data(self, key, value):
301 """Called in the EnvironBuilder to add files from the data dict.""" 317 """Called in the EnvironBuilder to add files from the data dict."""
302 if isinstance(value, tuple): 318 if isinstance(value, tuple):
303 self.files.add_file(key, *value) 319 self.files.add_file(key, *value)
304 elif isinstance(value, dict): 320 elif isinstance(value, dict):
305 from warnings import warn 321 from warnings import warn
306 warn(DeprecationWarning('it\'s no longer possible to pass dicts ' 322 warn(DeprecationWarning('it\'s no longer possible to pass dicts '
307 'as `data`. Use tuples or FileStorage ' 323 'as `data`. Use tuples or FileStorage '
308 'objects intead'), stacklevel=2) 324 'objects instead'), stacklevel=2)
309 args = v
310 value = dict(value) 325 value = dict(value)
311 mimetype = value.pop('mimetype', None) 326 mimetype = value.pop('mimetype', None)
312 if mimetype is not None: 327 if mimetype is not None:
313 value['content_type'] = mimetype 328 value['content_type'] = mimetype
314 self.files.add_file(key, **value) 329 self.files.add_file(key, **value)
341 del _get_base_url, _set_base_url 356 del _get_base_url, _set_base_url
342 357
343 def _get_content_type(self): 358 def _get_content_type(self):
344 ct = self.headers.get('Content-Type') 359 ct = self.headers.get('Content-Type')
345 if ct is None and not self._input_stream: 360 if ct is None and not self._input_stream:
346 if self.method in ('POST', 'PUT'): 361 if self.method in ('POST', 'PUT', 'PATCH'):
347 if self._files: 362 if self._files:
348 return 'multipart/form-data' 363 return 'multipart/form-data'
349 return 'application/x-www-form-urlencoded' 364 return 'application/x-www-form-urlencoded'
350 return None 365 return None
351 return ct 366 return ct
505 520
506 result = {} 521 result = {}
507 if self.environ_base: 522 if self.environ_base:
508 result.update(self.environ_base) 523 result.update(self.environ_base)
509 524
510 def _encode(x): 525 def _path_encode(x):
511 if isinstance(x, unicode): 526 if isinstance(x, unicode):
512 return x.encode(self.charset) 527 x = x.encode(self.charset)
513 return x 528 return _unquote(x)
514 529
515 result.update({ 530 result.update({
516 'REQUEST_METHOD': self.method, 531 'REQUEST_METHOD': self.method,
517 'SCRIPT_NAME': _encode(self.script_root), 532 'SCRIPT_NAME': _path_encode(self.script_root),
518 'PATH_INFO': _encode(self.path), 533 'PATH_INFO': _path_encode(self.path),
519 'QUERY_STRING': self.query_string, 534 'QUERY_STRING': self.query_string,
520 'SERVER_NAME': self.server_name, 535 'SERVER_NAME': self.server_name,
521 'SERVER_PORT': str(self.server_port), 536 'SERVER_PORT': str(self.server_port),
522 'HTTP_HOST': self.host, 537 'HTTP_HOST': self.host,
523 'SERVER_PROTOCOL': self.server_protocol, 538 'SERVER_PROTOCOL': self.server_protocol,
546 if cls is None: 561 if cls is None:
547 cls = self.request_class 562 cls = self.request_class
548 return cls(self.get_environ()) 563 return cls(self.get_environ())
549 564
550 565
566 class ClientRedirectError(Exception):
567 """
568 If a redirect loop is detected when using follow_redirects=True with
569 the :cls:`Client`, then this exception is raised.
570 """
571
572
551 class Client(object): 573 class Client(object):
552 """This class allows to send requests to a wrapped application. 574 """This class allows to send requests to a wrapped application.
553 575
554 The response wrapper can be a class or factory function that takes 576 The response wrapper can be a class or factory function that takes
555 three arguments: app_iter, status and headers. The default response 577 three arguments: app_iter, status and headers. The default response
564 586
565 The use_cookies parameter indicates whether cookies should be stored and 587 The use_cookies parameter indicates whether cookies should be stored and
566 sent for subsequent requests. This is True by default, but passing False 588 sent for subsequent requests. This is True by default, but passing False
567 will disable this behaviour. 589 will disable this behaviour.
568 590
591 If you want to request some subdomain of your application you may set
592 `allow_subdomain_redirects` to `True` as if not no external redirects
593 are allowed.
594
569 .. versionadded:: 0.5 595 .. versionadded:: 0.5
570 `use_cookies` is new in this version. Older versions did not provide 596 `use_cookies` is new in this version. Older versions did not provide
571 builtin cookie support. 597 builtin cookie support.
572 """ 598 """
573 599
574 def __init__(self, application, response_wrapper=None, use_cookies=True): 600 def __init__(self, application, response_wrapper=None, use_cookies=True,
601 allow_subdomain_redirects=False):
575 self.application = application 602 self.application = application
576 if response_wrapper is None: 603 if response_wrapper is None:
577 response_wrapper = lambda a, s, h: (a, s, h) 604 response_wrapper = lambda a, s, h: (a, s, h)
578 self.response_wrapper = response_wrapper 605 self.response_wrapper = response_wrapper
579 if use_cookies: 606 if use_cookies:
580 self.cookie_jar = _TestCookieJar() 607 self.cookie_jar = _TestCookieJar()
581 else: 608 else:
582 self.cookie_jar = None 609 self.cookie_jar = None
610 self.redirect_client = None
611 self.allow_subdomain_redirects = allow_subdomain_redirects
612
613 def set_cookie(self, server_name, key, value='', max_age=None,
614 expires=None, path='/', domain=None, secure=None,
615 httponly=False, charset='utf-8'):
616 """Sets a cookie in the client's cookie jar. The server name
617 is required and has to match the one that is also passed to
618 the open call.
619 """
620 assert self.cookie_jar is not None, 'cookies disabled'
621 header = dump_cookie(key, value, max_age, expires, path, domain,
622 secure, httponly, charset)
623 environ = create_environ(path, base_url='http://' + server_name)
624 headers = [('Set-Cookie', header)]
625 self.cookie_jar.extract_wsgi(environ, headers)
626
627 def delete_cookie(self, server_name, key, path='/', domain=None):
628 """Deletes a cookie in the test client."""
629 self.set_cookie(server_name, key, expires=0, max_age=0,
630 path=path, domain=domain)
583 631
584 def open(self, *args, **kwargs): 632 def open(self, *args, **kwargs):
585 """Takes the same arguments as the :class:`EnvironBuilder` class with 633 """Takes the same arguments as the :class:`EnvironBuilder` class with
586 some additions: You can provide a :class:`EnvironBuilder` or a WSGI 634 some additions: You can provide a :class:`EnvironBuilder` or a WSGI
587 environment as only argument instead of the :class:`EnvironBuilder` 635 environment as only argument instead of the :class:`EnvironBuilder`
598 The `follow_redirects` parameter was added to :func:`open`. 646 The `follow_redirects` parameter was added to :func:`open`.
599 647
600 Additional parameters: 648 Additional parameters:
601 649
602 :param as_tuple: Returns a tuple in the form ``(environ, result)`` 650 :param as_tuple: Returns a tuple in the form ``(environ, result)``
603 :param buffered: Set this to true to buffer the application run. 651 :param buffered: Set this to True to buffer the application run.
604 This will automatically close the application for 652 This will automatically close the application for
605 you as well. 653 you as well.
606 :param follow_redirects: Set this to True if the `Client` should 654 :param follow_redirects: Set this to True if the `Client` should
607 follow HTTP redirects. 655 follow HTTP redirects.
608 """ 656 """
630 678
631 # handle redirects 679 # handle redirects
632 redirect_chain = [] 680 redirect_chain = []
633 status_code = int(rv[1].split(None, 1)[0]) 681 status_code = int(rv[1].split(None, 1)[0])
634 while status_code in (301, 302, 303, 305, 307) and follow_redirects: 682 while status_code in (301, 302, 303, 305, 307) and follow_redirects:
683 if not self.redirect_client:
684 # assume that we're not using the user defined response wrapper
685 # so that we don't need any ugly hacks to get the status
686 # code from the response.
687 self.redirect_client = Client(self.application)
688 self.redirect_client.cookie_jar = self.cookie_jar
689
635 redirect = dict(rv[2])['Location'] 690 redirect = dict(rv[2])['Location']
636 host = get_host(create_environ('/', redirect)) 691
637 if get_host(environ).split(':', 1)[0] != host: 692 scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect)
693 base_url = urlparse.urlunsplit((scheme, netloc, '', '', '')).rstrip('/') + '/'
694
695 cur_server_name = netloc.split(':', 1)[0].split('.')
696 real_server_name = get_host(environ).split(':', 1)[0].split('.')
697
698 if self.allow_subdomain_redirects:
699 allowed = cur_server_name[-len(real_server_name):] == real_server_name
700 else:
701 allowed = cur_server_name == real_server_name
702
703 if not allowed:
638 raise RuntimeError('%r does not support redirect to ' 704 raise RuntimeError('%r does not support redirect to '
639 'external targets' % self.__class__) 705 'external targets' % self.__class__)
640 706
641 scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect)
642 redirect_chain.append((redirect, status_code)) 707 redirect_chain.append((redirect, status_code))
643 708
644 kwargs.update({ 709 # the redirect request should be a new request, and not be based on
645 'base_url': urlparse.urlunsplit((scheme, host, 710 # the old request
646 script_root, '', '')).rstrip('/') + '/', 711
712 redirect_kwargs = {
713 'path': script_root,
714 'base_url': base_url,
647 'query_string': qs, 715 'query_string': qs,
648 'as_tuple': as_tuple, 716 'as_tuple': True,
649 'buffered': buffered, 717 'buffered': buffered,
650 'follow_redirects': False 718 'follow_redirects': False,
651 }) 719 }
652 rv = self.open(*args, **kwargs) 720 environ, rv = self.redirect_client.open(**redirect_kwargs)
653 status_code = int(rv[1].split(None, 1)[0]) 721 status_code = int(rv[1].split(None, 1)[0])
654 722
655 # Prevent loops 723 # Prevent loops
656 if redirect_chain[-1] in redirect_chain[0:-1]: 724 if redirect_chain[-1] in redirect_chain[:-1]:
657 break 725 raise ClientRedirectError("loop detected")
658 726
659 response = self.response_wrapper(*rv) 727 response = self.response_wrapper(*rv)
660 if as_tuple: 728 if as_tuple:
661 return environ, response 729 return environ, response
662 return response 730 return response
663 731
664 def get(self, *args, **kw): 732 def get(self, *args, **kw):
665 """Like open but method is enforced to GET.""" 733 """Like open but method is enforced to GET."""
666 kw['method'] = 'GET' 734 kw['method'] = 'GET'
735 return self.open(*args, **kw)
736
737 def patch(self, *args, **kw):
738 """Like open but method is enforced to PATCH."""
739 kw['method'] = 'PATCH'
667 return self.open(*args, **kw) 740 return self.open(*args, **kw)
668 741
669 def post(self, *args, **kw): 742 def post(self, *args, **kw):
670 """Like open but method is enforced to POST.""" 743 """Like open but method is enforced to POST."""
671 kw['method'] = 'POST' 744 kw['method'] = 'POST'
730 803
731 :param app: the application to execute. 804 :param app: the application to execute.
732 :param buffered: set to `True` to enforce buffering. 805 :param buffered: set to `True` to enforce buffering.
733 :return: tuple in the form ``(app_iter, status, headers)`` 806 :return: tuple in the form ``(app_iter, status, headers)``
734 """ 807 """
808 environ = _get_environ(environ)
735 response = [] 809 response = []
736 buffer = [] 810 buffer = []
737 811
738 def start_response(status, headers, exc_info=None): 812 def start_response(status, headers, exc_info=None):
739 if exc_info is not None: 813 if exc_info is not None:
741 response[:] = [status, headers] 815 response[:] = [status, headers]
742 return buffer.append 816 return buffer.append
743 817
744 app_iter = app(environ, start_response) 818 app_iter = app(environ, start_response)
745 819
746 # when buffering we emit the close call early and conver the 820 # when buffering we emit the close call early and convert the
747 # application iterator into a regular list 821 # application iterator into a regular list
748 if buffered: 822 if buffered:
749 close_func = getattr(app_iter, 'close', None) 823 close_func = getattr(app_iter, 'close', None)
750 try: 824 try:
751 app_iter = list(app_iter) 825 app_iter = list(app_iter)
759 # we have a close callable. 833 # we have a close callable.
760 else: 834 else:
761 while not response: 835 while not response:
762 buffer.append(app_iter.next()) 836 buffer.append(app_iter.next())
763 if buffer: 837 if buffer:
838 close_func = getattr(app_iter, 'close', None)
764 app_iter = chain(buffer, app_iter) 839 app_iter = chain(buffer, app_iter)
765 close_func = getattr(app_iter, 'close', None)
766 if close_func is not None: 840 if close_func is not None:
767 app_iter = ClosingIterator(app_iter, close_func) 841 app_iter = ClosingIterator(app_iter, close_func)
768 842
769 return app_iter, response[0], response[1] 843 return app_iter, response[0], response[1]