comparison MoinMoin/support/werkzeug/test.py @ 6094:9f12f41504fc

upgrade werkzeug from 0.8.3 to 0.11.11 no other changes, does not work like this, see next commit.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Mon, 05 Sep 2016 23:25:59 +0200
parents 8de563c487be
children 7f12cf241d5e
comparison
equal deleted inserted replaced
6089:dfbc455e2c46 6094:9f12f41504fc
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) 2011 by the Werkzeug Team, see AUTHORS for more details. 8 :copyright: (c) 2014 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
13 import mimetypes 12 import mimetypes
14 from time import time 13 from time import time
15 from random import random 14 from random import random
16 from itertools import chain 15 from itertools import chain
17 from tempfile import TemporaryFile 16 from tempfile import TemporaryFile
18 from cStringIO import StringIO 17 from io import BytesIO
19 from cookielib import CookieJar 18
20 from urllib2 import Request as U2Request 19 try:
21 20 from urllib2 import Request as U2Request
21 except ImportError:
22 from urllib.request import Request as U2Request
23 try:
24 from http.cookiejar import CookieJar
25 except ImportError: # Py2
26 from cookielib import CookieJar
27
28 from werkzeug._compat import iterlists, iteritems, itervalues, to_bytes, \
29 string_types, text_type, reraise, wsgi_encoding_dance, \
30 make_literal_wrapper
22 from werkzeug._internal import _empty_stream, _get_environ 31 from werkzeug._internal import _empty_stream, _get_environ
23 from werkzeug.wrappers import BaseRequest 32 from werkzeug.wrappers import BaseRequest
24 from werkzeug.urls import url_encode, url_fix, iri_to_uri, _unquote 33 from werkzeug.urls import url_encode, url_fix, iri_to_uri, url_unquote, \
34 url_unparse, url_parse
25 from werkzeug.wsgi import get_host, get_current_url, ClosingIterator 35 from werkzeug.wsgi import get_host, get_current_url, ClosingIterator
26 from werkzeug.utils import dump_cookie 36 from werkzeug.utils import dump_cookie
27 from werkzeug.datastructures import FileMultiDict, MultiDict, \ 37 from werkzeug.datastructures import FileMultiDict, MultiDict, \
28 CombinedMultiDict, Headers, FileStorage 38 CombinedMultiDict, Headers, FileStorage
29 39
30 40
31 def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500, 41 def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
32 boundary=None, charset='utf-8'): 42 boundary=None, charset='utf-8'):
33 """Encode a dict of values (either strings or file descriptors or 43 """Encode a dict of values (either strings or file descriptors or
34 :class:`FileStorage` objects.) into a multipart encoded string stored 44 :class:`FileStorage` objects.) into a multipart encoded string stored
35 in a file descriptor. 45 in a file descriptor.
36 """ 46 """
37 if boundary is None: 47 if boundary is None:
38 boundary = '---------------WerkzeugFormPart_%s%s' % (time(), random()) 48 boundary = '---------------WerkzeugFormPart_%s%s' % (time(), random())
39 _closure = [StringIO(), 0, False] 49 _closure = [BytesIO(), 0, False]
40 50
41 if use_tempfile: 51 if use_tempfile:
42 def write(string): 52 def write_binary(string):
43 stream, total_length, on_disk = _closure 53 stream, total_length, on_disk = _closure
44 if on_disk: 54 if on_disk:
45 stream.write(string) 55 stream.write(string)
46 else: 56 else:
47 length = len(string) 57 length = len(string)
53 new_stream.write(string) 63 new_stream.write(string)
54 _closure[0] = new_stream 64 _closure[0] = new_stream
55 _closure[2] = True 65 _closure[2] = True
56 _closure[1] = total_length + length 66 _closure[1] = total_length + length
57 else: 67 else:
58 write = _closure[0].write 68 write_binary = _closure[0].write
69
70 def write(string):
71 write_binary(string.encode(charset))
59 72
60 if not isinstance(values, MultiDict): 73 if not isinstance(values, MultiDict):
61 values = MultiDict(values) 74 values = MultiDict(values)
62 75
63 for key, values in values.iterlists(): 76 for key, values in iterlists(values):
64 for value in values: 77 for value in values:
65 write('--%s\r\nContent-Disposition: form-data; name="%s"' % 78 write('--%s\r\nContent-Disposition: form-data; name="%s"' %
66 (boundary, key)) 79 (boundary, key))
67 reader = getattr(value, 'read', None) 80 reader = getattr(value, 'read', None)
68 if reader is not None: 81 if reader is not None:
80 write('Content-Type: %s\r\n\r\n' % content_type) 93 write('Content-Type: %s\r\n\r\n' % content_type)
81 while 1: 94 while 1:
82 chunk = reader(16384) 95 chunk = reader(16384)
83 if not chunk: 96 if not chunk:
84 break 97 break
85 write(chunk) 98 write_binary(chunk)
86 else: 99 else:
87 if isinstance(value, unicode): 100 if not isinstance(value, string_types):
88 value = value.encode(charset) 101 value = str(value)
89 write('\r\n\r\n' + str(value)) 102
103 value = to_bytes(value, charset)
104 write('\r\n\r\n')
105 write_binary(value)
90 write('\r\n') 106 write('\r\n')
91 write('--%s--\r\n' % boundary) 107 write('--%s--\r\n' % boundary)
92 108
93 length = int(_closure[0].tell()) 109 length = int(_closure[0].tell())
94 _closure[0].seek(0) 110 _closure[0].seek(0)
111 'EnvironBuilder or FileStorage instead')) 127 'EnvironBuilder or FileStorage instead'))
112 return FileStorage(fd, filename=filename, content_type=mimetype) 128 return FileStorage(fd, filename=filename, content_type=mimetype)
113 129
114 130
115 class _TestCookieHeaders(object): 131 class _TestCookieHeaders(object):
132
116 """A headers adapter for cookielib 133 """A headers adapter for cookielib
117 """ 134 """
118 135
119 def __init__(self, headers): 136 def __init__(self, headers):
120 self.headers = headers 137 self.headers = headers
125 for k, v in self.headers: 142 for k, v in self.headers:
126 if k.lower() == name: 143 if k.lower() == name:
127 headers.append(v) 144 headers.append(v)
128 return headers 145 return headers
129 146
147 def get_all(self, name, default=None):
148 rv = []
149 for k, v in self.headers:
150 if k.lower() == name.lower():
151 rv.append(v)
152 return rv or default or []
153
130 154
131 class _TestCookieResponse(object): 155 class _TestCookieResponse(object):
156
132 """Something that looks like a httplib.HTTPResponse, but is actually just an 157 """Something that looks like a httplib.HTTPResponse, but is actually just an
133 adapter for our test responses to make them available for cookielib. 158 adapter for our test responses to make them available for cookielib.
134 """ 159 """
135 160
136 def __init__(self, headers): 161 def __init__(self, headers):
139 def info(self): 164 def info(self):
140 return self.headers 165 return self.headers
141 166
142 167
143 class _TestCookieJar(CookieJar): 168 class _TestCookieJar(CookieJar):
169
144 """A cookielib.CookieJar modified to inject and read cookie headers from 170 """A cookielib.CookieJar modified to inject and read cookie headers from
145 and to wsgi environments, and wsgi application responses. 171 and to wsgi environments, and wsgi application responses.
146 """ 172 """
147 173
148 def inject_wsgi(self, environ): 174 def inject_wsgi(self, environ):
169 """Iterates over a dict or multidict yielding all keys and values. 195 """Iterates over a dict or multidict yielding all keys and values.
170 This is used to iterate over the data passed to the 196 This is used to iterate over the data passed to the
171 :class:`EnvironBuilder`. 197 :class:`EnvironBuilder`.
172 """ 198 """
173 if isinstance(data, MultiDict): 199 if isinstance(data, MultiDict):
174 for key, values in data.iterlists(): 200 for key, values in iterlists(data):
175 for value in values: 201 for value in values:
176 yield key, value 202 yield key, value
177 else: 203 else:
178 for key, values in data.iteritems(): 204 for key, values in iteritems(data):
179 if isinstance(values, list): 205 if isinstance(values, list):
180 for value in values: 206 for value in values:
181 yield key, value 207 yield key, value
182 else: 208 else:
183 yield key, values 209 yield key, values
184 210
185 211
186 class EnvironBuilder(object): 212 class EnvironBuilder(object):
213
187 """This class can be used to conveniently create a WSGI environment 214 """This class can be used to conveniently create a WSGI environment
188 for testing purposes. It can be used to quickly create WSGI environments 215 for testing purposes. It can be used to quickly create WSGI environments
189 or request objects from arbitrary data. 216 or request objects from arbitrary data.
190 217
191 The signature of this class is also used in some other places as of 218 The signature of this class is also used in some other places as of
257 def __init__(self, path='/', base_url=None, query_string=None, 284 def __init__(self, path='/', base_url=None, query_string=None,
258 method='GET', input_stream=None, content_type=None, 285 method='GET', input_stream=None, content_type=None,
259 content_length=None, errors_stream=None, multithread=False, 286 content_length=None, errors_stream=None, multithread=False,
260 multiprocess=False, run_once=False, headers=None, data=None, 287 multiprocess=False, run_once=False, headers=None, data=None,
261 environ_base=None, environ_overrides=None, charset='utf-8'): 288 environ_base=None, environ_overrides=None, charset='utf-8'):
262 if query_string is None and '?' in path: 289 path_s = make_literal_wrapper(path)
263 path, query_string = path.split('?', 1) 290 if query_string is None and path_s('?') in path:
291 path, query_string = path.split(path_s('?'), 1)
264 self.charset = charset 292 self.charset = charset
265 if isinstance(path, unicode): 293 self.path = iri_to_uri(path)
266 path = iri_to_uri(path, charset)
267 self.path = path
268 if base_url is not None: 294 if base_url is not None:
269 if isinstance(base_url, unicode): 295 base_url = url_fix(iri_to_uri(base_url, charset), charset)
270 base_url = iri_to_uri(base_url, charset)
271 else:
272 base_url = url_fix(base_url, charset)
273 self.base_url = base_url 296 self.base_url = base_url
274 if isinstance(query_string, basestring): 297 if isinstance(query_string, (bytes, text_type)):
275 self.query_string = query_string 298 self.query_string = query_string
276 else: 299 else:
277 if query_string is None: 300 if query_string is None:
278 query_string = MultiDict() 301 query_string = MultiDict()
279 elif not isinstance(query_string, MultiDict): 302 elif not isinstance(query_string, MultiDict):
283 if headers is None: 306 if headers is None:
284 headers = Headers() 307 headers = Headers()
285 elif not isinstance(headers, Headers): 308 elif not isinstance(headers, Headers):
286 headers = Headers(headers) 309 headers = Headers(headers)
287 self.headers = headers 310 self.headers = headers
288 self.content_type = content_type 311 if content_type is not None:
312 self.content_type = content_type
289 if errors_stream is None: 313 if errors_stream is None:
290 errors_stream = sys.stderr 314 errors_stream = sys.stderr
291 self.errors_stream = errors_stream 315 self.errors_stream = errors_stream
292 self.multithread = multithread 316 self.multithread = multithread
293 self.multiprocess = multiprocess 317 self.multiprocess = multiprocess
299 self.closed = False 323 self.closed = False
300 324
301 if data: 325 if data:
302 if input_stream is not None: 326 if input_stream is not None:
303 raise TypeError('can\'t provide input stream and data') 327 raise TypeError('can\'t provide input stream and data')
304 if isinstance(data, basestring): 328 if isinstance(data, text_type):
305 self.input_stream = StringIO(data) 329 data = data.encode(self.charset)
330 if isinstance(data, bytes):
331 self.input_stream = BytesIO(data)
306 if self.content_length is None: 332 if self.content_length is None:
307 self.content_length = len(data) 333 self.content_length = len(data)
308 else: 334 else:
309 for key, value in _iter_data(data): 335 for key, value in _iter_data(data):
310 if isinstance(value, (tuple, dict)) or \ 336 if isinstance(value, (tuple, dict)) or \
329 self.files.add_file(key, **value) 355 self.files.add_file(key, **value)
330 else: 356 else:
331 self.files.add_file(key, value) 357 self.files.add_file(key, value)
332 358
333 def _get_base_url(self): 359 def _get_base_url(self):
334 return urlparse.urlunsplit((self.url_scheme, self.host, 360 return url_unparse((self.url_scheme, self.host,
335 self.script_root, '', '')).rstrip('/') + '/' 361 self.script_root, '', '')).rstrip('/') + '/'
336 362
337 def _set_base_url(self, value): 363 def _set_base_url(self, value):
338 if value is None: 364 if value is None:
339 scheme = 'http' 365 scheme = 'http'
340 netloc = 'localhost' 366 netloc = 'localhost'
341 scheme = 'http'
342 script_root = '' 367 script_root = ''
343 else: 368 else:
344 scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(value) 369 scheme, netloc, script_root, qs, anchor = url_parse(value)
345 if qs or anchor: 370 if qs or anchor:
346 raise ValueError('base url must not contain a query string ' 371 raise ValueError('base url must not contain a query string '
347 'or fragment') 372 'or fragment')
348 self.script_root = script_root.rstrip('/') 373 self.script_root = script_root.rstrip('/')
349 self.host = netloc 374 self.host = netloc
356 del _get_base_url, _set_base_url 381 del _get_base_url, _set_base_url
357 382
358 def _get_content_type(self): 383 def _get_content_type(self):
359 ct = self.headers.get('Content-Type') 384 ct = self.headers.get('Content-Type')
360 if ct is None and not self._input_stream: 385 if ct is None and not self._input_stream:
361 if self.method in ('POST', 'PUT', 'PATCH'): 386 if self._files:
362 if self._files: 387 return 'multipart/form-data'
363 return 'multipart/form-data' 388 elif self._form:
364 return 'application/x-www-form-urlencoded' 389 return 'application/x-www-form-urlencoded'
365 return None 390 return None
366 return ct 391 return ct
367 392
368 def _set_content_type(self, value): 393 def _set_content_type(self, value):
392 :attr:`form` for auto detection.''') 417 :attr:`form` for auto detection.''')
393 del _get_content_length, _set_content_length 418 del _get_content_length, _set_content_length
394 419
395 def form_property(name, storage, doc): 420 def form_property(name, storage, doc):
396 key = '_' + name 421 key = '_' + name
422
397 def getter(self): 423 def getter(self):
398 if self._input_stream is not None: 424 if self._input_stream is not None:
399 raise AttributeError('an input stream is defined') 425 raise AttributeError('an input stream is defined')
400 rv = getattr(self, key) 426 rv = getattr(self, key)
401 if rv is None: 427 if rv is None:
402 rv = storage() 428 rv = storage()
403 setattr(self, key, rv) 429 setattr(self, key, rv)
430
404 return rv 431 return rv
432
405 def setter(self, value): 433 def setter(self, value):
406 self._input_stream = None 434 self._input_stream = None
407 setattr(self, key, value) 435 setattr(self, key, value)
408 return property(getter, setter, doc) 436 return property(getter, setter, doc)
409 437
472 elif self.url_scheme == 'https': 500 elif self.url_scheme == 'https':
473 return 443 501 return 443
474 return 80 502 return 80
475 503
476 def __del__(self): 504 def __del__(self):
477 self.close() 505 try:
506 self.close()
507 except Exception:
508 pass
478 509
479 def close(self): 510 def close(self):
480 """Closes all files. If you put real :class:`file` objects into the 511 """Closes all files. If you put real :class:`file` objects into the
481 :attr:`files` dict you can call this method to automatically close 512 :attr:`files` dict you can call this method to automatically close
482 them all in one go. 513 them all in one go.
483 """ 514 """
484 if self.closed: 515 if self.closed:
485 return 516 return
486 try: 517 try:
487 files = self.files.itervalues() 518 files = itervalues(self.files)
488 except AttributeError: 519 except AttributeError:
489 files = () 520 files = ()
490 for f in files: 521 for f in files:
491 try: 522 try:
492 f.close() 523 f.close()
493 except Exception, e: 524 except Exception:
494 pass 525 pass
495 self.closed = True 526 self.closed = True
496 527
497 def get_environ(self): 528 def get_environ(self):
498 """Return the built environ.""" 529 """Return the built environ."""
510 values = CombinedMultiDict([self.form, self.files]) 541 values = CombinedMultiDict([self.form, self.files])
511 input_stream, content_length, boundary = \ 542 input_stream, content_length, boundary = \
512 stream_encode_multipart(values, charset=self.charset) 543 stream_encode_multipart(values, charset=self.charset)
513 content_type += '; boundary="%s"' % boundary 544 content_type += '; boundary="%s"' % boundary
514 elif content_type == 'application/x-www-form-urlencoded': 545 elif content_type == 'application/x-www-form-urlencoded':
546 # XXX: py2v3 review
515 values = url_encode(self.form, charset=self.charset) 547 values = url_encode(self.form, charset=self.charset)
548 values = values.encode('ascii')
516 content_length = len(values) 549 content_length = len(values)
517 input_stream = StringIO(values) 550 input_stream = BytesIO(values)
518 else: 551 else:
519 input_stream = _empty_stream 552 input_stream = _empty_stream
520 553
521 result = {} 554 result = {}
522 if self.environ_base: 555 if self.environ_base:
523 result.update(self.environ_base) 556 result.update(self.environ_base)
524 557
525 def _path_encode(x): 558 def _path_encode(x):
526 if isinstance(x, unicode): 559 return wsgi_encoding_dance(url_unquote(x, self.charset), self.charset)
527 x = x.encode(self.charset) 560
528 return _unquote(x) 561 qs = wsgi_encoding_dance(self.query_string)
529 562
530 result.update({ 563 result.update({
531 'REQUEST_METHOD': self.method, 564 'REQUEST_METHOD': self.method,
532 'SCRIPT_NAME': _path_encode(self.script_root), 565 'SCRIPT_NAME': _path_encode(self.script_root),
533 'PATH_INFO': _path_encode(self.path), 566 'PATH_INFO': _path_encode(self.path),
534 'QUERY_STRING': self.query_string, 567 'QUERY_STRING': qs,
535 'SERVER_NAME': self.server_name, 568 'SERVER_NAME': self.server_name,
536 'SERVER_PORT': str(self.server_port), 569 'SERVER_PORT': str(self.server_port),
537 'HTTP_HOST': self.host, 570 'HTTP_HOST': self.host,
538 'SERVER_PROTOCOL': self.server_protocol, 571 'SERVER_PROTOCOL': self.server_protocol,
539 'CONTENT_TYPE': content_type or '', 572 'CONTENT_TYPE': content_type or '',
544 'wsgi.errors': self.errors_stream, 577 'wsgi.errors': self.errors_stream,
545 'wsgi.multithread': self.multithread, 578 'wsgi.multithread': self.multithread,
546 'wsgi.multiprocess': self.multiprocess, 579 'wsgi.multiprocess': self.multiprocess,
547 'wsgi.run_once': self.run_once 580 'wsgi.run_once': self.run_once
548 }) 581 })
549 for key, value in self.headers.to_list(self.charset): 582 for key, value in self.headers.to_wsgi_list():
550 result['HTTP_%s' % key.upper().replace('-', '_')] = value 583 result['HTTP_%s' % key.upper().replace('-', '_')] = value
551 if self.environ_overrides: 584 if self.environ_overrides:
552 result.update(self.environ_overrides) 585 result.update(self.environ_overrides)
553 return result 586 return result
554 587
562 cls = self.request_class 595 cls = self.request_class
563 return cls(self.get_environ()) 596 return cls(self.get_environ())
564 597
565 598
566 class ClientRedirectError(Exception): 599 class ClientRedirectError(Exception):
600
567 """ 601 """
568 If a redirect loop is detected when using follow_redirects=True with 602 If a redirect loop is detected when using follow_redirects=True with
569 the :cls:`Client`, then this exception is raised. 603 the :cls:`Client`, then this exception is raised.
570 """ 604 """
571 605
572 606
573 class Client(object): 607 class Client(object):
608
574 """This class allows to send requests to a wrapped application. 609 """This class allows to send requests to a wrapped application.
575 610
576 The response wrapper can be a class or factory function that takes 611 The response wrapper can be a class or factory function that takes
577 three arguments: app_iter, status and headers. The default response 612 three arguments: app_iter, status and headers. The default response
578 wrapper just returns a tuple. 613 wrapper just returns a tuple.
598 """ 633 """
599 634
600 def __init__(self, application, response_wrapper=None, use_cookies=True, 635 def __init__(self, application, response_wrapper=None, use_cookies=True,
601 allow_subdomain_redirects=False): 636 allow_subdomain_redirects=False):
602 self.application = application 637 self.application = application
603 if response_wrapper is None:
604 response_wrapper = lambda a, s, h: (a, s, h)
605 self.response_wrapper = response_wrapper 638 self.response_wrapper = response_wrapper
606 if use_cookies: 639 if use_cookies:
607 self.cookie_jar = _TestCookieJar() 640 self.cookie_jar = _TestCookieJar()
608 else: 641 else:
609 self.cookie_jar = None 642 self.cookie_jar = None
610 self.redirect_client = None
611 self.allow_subdomain_redirects = allow_subdomain_redirects 643 self.allow_subdomain_redirects = allow_subdomain_redirects
612 644
613 def set_cookie(self, server_name, key, value='', max_age=None, 645 def set_cookie(self, server_name, key, value='', max_age=None,
614 expires=None, path='/', domain=None, secure=None, 646 expires=None, path='/', domain=None, secure=None,
615 httponly=False, charset='utf-8'): 647 httponly=False, charset='utf-8'):
626 658
627 def delete_cookie(self, server_name, key, path='/', domain=None): 659 def delete_cookie(self, server_name, key, path='/', domain=None):
628 """Deletes a cookie in the test client.""" 660 """Deletes a cookie in the test client."""
629 self.set_cookie(server_name, key, expires=0, max_age=0, 661 self.set_cookie(server_name, key, expires=0, max_age=0,
630 path=path, domain=domain) 662 path=path, domain=domain)
663
664 def run_wsgi_app(self, environ, buffered=False):
665 """Runs the wrapped WSGI app with the given environment."""
666 if self.cookie_jar is not None:
667 self.cookie_jar.inject_wsgi(environ)
668 rv = run_wsgi_app(self.application, environ, buffered=buffered)
669 if self.cookie_jar is not None:
670 self.cookie_jar.extract_wsgi(environ, rv[2])
671 return rv
672
673 def resolve_redirect(self, response, new_location, environ, buffered=False):
674 """Resolves a single redirect and triggers the request again
675 directly on this redirect client.
676 """
677 scheme, netloc, script_root, qs, anchor = url_parse(new_location)
678 base_url = url_unparse((scheme, netloc, '', '', '')).rstrip('/') + '/'
679
680 cur_server_name = netloc.split(':', 1)[0].split('.')
681 real_server_name = get_host(environ).rsplit(':', 1)[0].split('.')
682
683 if self.allow_subdomain_redirects:
684 allowed = cur_server_name[-len(real_server_name):] == real_server_name
685 else:
686 allowed = cur_server_name == real_server_name
687
688 if not allowed:
689 raise RuntimeError('%r does not support redirect to '
690 'external targets' % self.__class__)
691
692 status_code = int(response[1].split(None, 1)[0])
693 if status_code == 307:
694 method = environ['REQUEST_METHOD']
695 else:
696 method = 'GET'
697
698 # For redirect handling we temporarily disable the response
699 # wrapper. This is not threadsafe but not a real concern
700 # since the test client must not be shared anyways.
701 old_response_wrapper = self.response_wrapper
702 self.response_wrapper = None
703 try:
704 return self.open(path=script_root, base_url=base_url,
705 query_string=qs, as_tuple=True,
706 buffered=buffered, method=method)
707 finally:
708 self.response_wrapper = old_response_wrapper
631 709
632 def open(self, *args, **kwargs): 710 def open(self, *args, **kwargs):
633 """Takes the same arguments as the :class:`EnvironBuilder` class with 711 """Takes the same arguments as the :class:`EnvironBuilder` class with
634 some additions: You can provide a :class:`EnvironBuilder` or a WSGI 712 some additions: You can provide a :class:`EnvironBuilder` or a WSGI
635 environment as only argument instead of the :class:`EnvironBuilder` 713 environment as only argument instead of the :class:`EnvironBuilder`
668 try: 746 try:
669 environ = builder.get_environ() 747 environ = builder.get_environ()
670 finally: 748 finally:
671 builder.close() 749 builder.close()
672 750
673 if self.cookie_jar is not None: 751 response = self.run_wsgi_app(environ, buffered=buffered)
674 self.cookie_jar.inject_wsgi(environ)
675 rv = run_wsgi_app(self.application, environ, buffered=buffered)
676 if self.cookie_jar is not None:
677 self.cookie_jar.extract_wsgi(environ, rv[2])
678 752
679 # handle redirects 753 # handle redirects
680 redirect_chain = [] 754 redirect_chain = []
681 status_code = int(rv[1].split(None, 1)[0]) 755 while 1:
682 while status_code in (301, 302, 303, 305, 307) and follow_redirects: 756 status_code = int(response[1].split(None, 1)[0])
683 if not self.redirect_client: 757 if status_code not in (301, 302, 303, 305, 307) \
684 # assume that we're not using the user defined response wrapper 758 or not follow_redirects:
685 # so that we don't need any ugly hacks to get the status 759 break
686 # code from the response. 760 new_location = response[2]['location']
687 self.redirect_client = Client(self.application) 761 new_redirect_entry = (new_location, status_code)
688 self.redirect_client.cookie_jar = self.cookie_jar 762 if new_redirect_entry in redirect_chain:
689 763 raise ClientRedirectError('loop detected')
690 redirect = dict(rv[2])['Location'] 764 redirect_chain.append(new_redirect_entry)
691 765 environ, response = self.resolve_redirect(response, new_location,
692 scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect) 766 environ,
693 base_url = urlparse.urlunsplit((scheme, netloc, '', '', '')).rstrip('/') + '/' 767 buffered=buffered)
694 768
695 cur_server_name = netloc.split(':', 1)[0].split('.') 769 if self.response_wrapper is not None:
696 real_server_name = get_host(environ).split(':', 1)[0].split('.') 770 response = self.response_wrapper(*response)
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:
704 raise RuntimeError('%r does not support redirect to '
705 'external targets' % self.__class__)
706
707 redirect_chain.append((redirect, status_code))
708
709 # the redirect request should be a new request, and not be based on
710 # the old request
711
712 redirect_kwargs = {
713 'path': script_root,
714 'base_url': base_url,
715 'query_string': qs,
716 'as_tuple': True,
717 'buffered': buffered,
718 'follow_redirects': False,
719 }
720 environ, rv = self.redirect_client.open(**redirect_kwargs)
721 status_code = int(rv[1].split(None, 1)[0])
722
723 # Prevent loops
724 if redirect_chain[-1] in redirect_chain[:-1]:
725 raise ClientRedirectError("loop detected")
726
727 response = self.response_wrapper(*rv)
728 if as_tuple: 771 if as_tuple:
729 return environ, response 772 return environ, response
730 return response 773 return response
731 774
732 def get(self, *args, **kw): 775 def get(self, *args, **kw):
755 return self.open(*args, **kw) 798 return self.open(*args, **kw)
756 799
757 def delete(self, *args, **kw): 800 def delete(self, *args, **kw):
758 """Like open but method is enforced to DELETE.""" 801 """Like open but method is enforced to DELETE."""
759 kw['method'] = 'DELETE' 802 kw['method'] = 'DELETE'
803 return self.open(*args, **kw)
804
805 def options(self, *args, **kw):
806 """Like open but method is enforced to OPTIONS."""
807 kw['method'] = 'OPTIONS'
808 return self.open(*args, **kw)
809
810 def trace(self, *args, **kw):
811 """Like open but method is enforced to TRACE."""
812 kw['method'] = 'TRACE'
760 return self.open(*args, **kw) 813 return self.open(*args, **kw)
761 814
762 def __repr__(self): 815 def __repr__(self):
763 return '<%s %r>' % ( 816 return '<%s %r>' % (
764 self.__class__.__name__, 817 self.__class__.__name__,
809 response = [] 862 response = []
810 buffer = [] 863 buffer = []
811 864
812 def start_response(status, headers, exc_info=None): 865 def start_response(status, headers, exc_info=None):
813 if exc_info is not None: 866 if exc_info is not None:
814 raise exc_info[0], exc_info[1], exc_info[2] 867 reraise(*exc_info)
815 response[:] = [status, headers] 868 response[:] = [status, headers]
816 return buffer.append 869 return buffer.append
817 870
818 app_iter = app(environ, start_response) 871 app_rv = app(environ, start_response)
872 close_func = getattr(app_rv, 'close', None)
873 app_iter = iter(app_rv)
819 874
820 # when buffering we emit the close call early and convert the 875 # when buffering we emit the close call early and convert the
821 # application iterator into a regular list 876 # application iterator into a regular list
822 if buffered: 877 if buffered:
823 close_func = getattr(app_iter, 'close', None)
824 try: 878 try:
825 app_iter = list(app_iter) 879 app_iter = list(app_iter)
826 finally: 880 finally:
827 if close_func is not None: 881 if close_func is not None:
828 close_func() 882 close_func()
829 883
830 # otherwise we iterate the application iter until we have 884 # otherwise we iterate the application iter until we have a response, chain
831 # a response, chain the already received data with the already 885 # the already received data with the already collected data and wrap it in
832 # collected data and wrap it in a new `ClosingIterator` if 886 # a new `ClosingIterator` if we need to restore a `close` callable from the
833 # we have a close callable. 887 # original return value.
834 else: 888 else:
835 while not response: 889 while not response:
836 buffer.append(app_iter.next()) 890 buffer.append(next(app_iter))
837 if buffer: 891 if buffer:
838 close_func = getattr(app_iter, 'close', None)
839 app_iter = chain(buffer, app_iter) 892 app_iter = chain(buffer, app_iter)
840 if close_func is not None: 893 if close_func is not None and app_iter is not app_rv:
841 app_iter = ClosingIterator(app_iter, close_func) 894 app_iter = ClosingIterator(app_iter, close_func)
842 895
843 return app_iter, response[0], response[1] 896 return app_iter, response[0], Headers(response[1])