1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) 2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php 3 4 """ 5 A module of many disparate routines. 6 """ 7 8 from __future__ import print_function 9 10 # functions which moved to paste.request and paste.response 11 # Deprecated around 15 Dec 2005 12 from paste.request import get_cookies, parse_querystring, parse_formvars 13 from paste.request import construct_url, path_info_split, path_info_pop 14 from paste.response import HeaderDict, has_header, header_value, remove_header 15 from paste.response import error_body_response, error_response, error_response_app 16 17 from traceback import print_exception 18 import six 19 import sys 20 from six.moves import cStringIO as StringIO 21 from six.moves.urllib.parse import unquote, urlsplit 22 import warnings 23 24 __all__ = ['add_close', 'add_start_close', 'capture_output', 'catch_errors', 25 'catch_errors_app', 'chained_app_iters', 'construct_url', 26 'dump_environ', 'encode_unicode_app_iter', 'error_body_response', 27 'error_response', 'get_cookies', 'has_header', 'header_value', 28 'interactive', 'intercept_output', 'path_info_pop', 29 'path_info_split', 'raw_interactive', 'send_file'] 30 31 class add_close(object): 32 """ 33 An an iterable that iterates over app_iter, then calls 34 close_func. 35 """ 36 37 def __init__(self, app_iterable, close_func): 38 self.app_iterable = app_iterable 39 self.app_iter = iter(app_iterable) 40 self.close_func = close_func 41 self._closed = False 42 43 def __iter__(self): 44 return self 45 46 def next(self): 47 return self.app_iter.next() 48 49 def close(self): 50 self._closed = True 51 if hasattr(self.app_iterable, 'close'): 52 self.app_iterable.close() 53 self.close_func() 54 55 def __del__(self): 56 if not self._closed: 57 # We can't raise an error or anything at this stage 58 print("Error: app_iter.close() was not called when finishing " 59 "WSGI request. finalization function %s not called" 60 % self.close_func, file=sys.stderr) 61 62 class add_start_close(object): 63 """ 64 An an iterable that iterates over app_iter, calls start_func 65 before the first item is returned, then calls close_func at the 66 end. 67 """ 68 69 def __init__(self, app_iterable, start_func, close_func=None): 70 self.app_iterable = app_iterable 71 self.app_iter = iter(app_iterable) 72 self.first = True 73 self.start_func = start_func 74 self.close_func = close_func 75 self._closed = False 76 77 def __iter__(self): 78 return self 79 80 def next(self): 81 if self.first: 82 self.start_func() 83 self.first = False 84 return next(self.app_iter) 85 __next__ = next 86 87 def close(self): 88 self._closed = True 89 if hasattr(self.app_iterable, 'close'): 90 self.app_iterable.close() 91 if self.close_func is not None: 92 self.close_func() 93 94 def __del__(self): 95 if not self._closed: 96 # We can't raise an error or anything at this stage 97 print("Error: app_iter.close() was not called when finishing " 98 "WSGI request. finalization function %s not called" 99 % self.close_func, file=sys.stderr) 100 101 class chained_app_iters(object): 102 103 """ 104 Chains several app_iters together, also delegating .close() to each 105 of them. 106 """ 107 108 def __init__(self, *chained): 109 self.app_iters = chained 110 self.chained = [iter(item) for item in chained] 111 self._closed = False 112 113 def __iter__(self): 114 return self 115 116 def next(self): 117 if len(self.chained) == 1: 118 return self.chained[0].next() 119 else: 120 try: 121 return self.chained[0].next() 122 except StopIteration: 123 self.chained.pop(0) 124 return self.next() 125 126 def close(self): 127 self._closed = True 128 got_exc = None 129 for app_iter in self.app_iters: 130 try: 131 if hasattr(app_iter, 'close'): 132 app_iter.close() 133 except: 134 got_exc = sys.exc_info() 135 if got_exc: 136 six.reraise(got_exc[0], got_exc[1], got_exc[2]) 137 138 def __del__(self): 139 if not self._closed: 140 # We can't raise an error or anything at this stage 141 print("Error: app_iter.close() was not called when finishing " 142 "WSGI request. finalization function %s not called" 143 % self.close_func, file=sys.stderr) 144 145 class encode_unicode_app_iter(object): 146 """ 147 Encodes an app_iterable's unicode responses as strings 148 """ 149 150 def __init__(self, app_iterable, encoding=sys.getdefaultencoding(), 151 errors='strict'): 152 self.app_iterable = app_iterable 153 self.app_iter = iter(app_iterable) 154 self.encoding = encoding 155 self.errors = errors 156 157 def __iter__(self): 158 return self 159 160 def next(self): 161 content = next(self.app_iter) 162 if isinstance(content, six.text_type): 163 content = content.encode(self.encoding, self.errors) 164 return content 165 __next__ = next 166 167 def close(self): 168 if hasattr(self.app_iterable, 'close'): 169 self.app_iterable.close() 170 171 def catch_errors(application, environ, start_response, error_callback, 172 ok_callback=None): 173 """ 174 Runs the application, and returns the application iterator (which should be 175 passed upstream). If an error occurs then error_callback will be called with 176 exc_info as its sole argument. If no errors occur and ok_callback is given, 177 then it will be called with no arguments. 178 """ 179 try: 180 app_iter = application(environ, start_response) 181 except: 182 error_callback(sys.exc_info()) 183 raise 184 if type(app_iter) in (list, tuple): 185 # These won't produce exceptions 186 if ok_callback: 187 ok_callback() 188 return app_iter 189 else: 190 return _wrap_app_iter(app_iter, error_callback, ok_callback) 191 192 class _wrap_app_iter(object): 193 194 def __init__(self, app_iterable, error_callback, ok_callback): 195 self.app_iterable = app_iterable 196 self.app_iter = iter(app_iterable) 197 self.error_callback = error_callback 198 self.ok_callback = ok_callback 199 if hasattr(self.app_iterable, 'close'): 200 self.close = self.app_iterable.close 201 202 def __iter__(self): 203 return self 204 205 def next(self): 206 try: 207 return self.app_iter.next() 208 except StopIteration: 209 if self.ok_callback: 210 self.ok_callback() 211 raise 212 except: 213 self.error_callback(sys.exc_info()) 214 raise 215 216 def catch_errors_app(application, environ, start_response, error_callback_app, 217 ok_callback=None, catch=Exception): 218 """ 219 Like ``catch_errors``, except error_callback_app should be a 220 callable that will receive *three* arguments -- ``environ``, 221 ``start_response``, and ``exc_info``. It should call 222 ``start_response`` (*with* the exc_info argument!) and return an 223 iterator. 224 """ 225 try: 226 app_iter = application(environ, start_response) 227 except catch: 228 return error_callback_app(environ, start_response, sys.exc_info()) 229 if type(app_iter) in (list, tuple): 230 # These won't produce exceptions 231 if ok_callback is not None: 232 ok_callback() 233 return app_iter 234 else: 235 return _wrap_app_iter_app( 236 environ, start_response, app_iter, 237 error_callback_app, ok_callback, catch=catch) 238 239 class _wrap_app_iter_app(object): 240 241 def __init__(self, environ, start_response, app_iterable, 242 error_callback_app, ok_callback, catch=Exception): 243 self.environ = environ 244 self.start_response = start_response 245 self.app_iterable = app_iterable 246 self.app_iter = iter(app_iterable) 247 self.error_callback_app = error_callback_app 248 self.ok_callback = ok_callback 249 self.catch = catch 250 if hasattr(self.app_iterable, 'close'): 251 self.close = self.app_iterable.close 252 253 def __iter__(self): 254 return self 255 256 def next(self): 257 try: 258 return self.app_iter.next() 259 except StopIteration: 260 if self.ok_callback: 261 self.ok_callback() 262 raise 263 except self.catch: 264 if hasattr(self.app_iterable, 'close'): 265 try: 266 self.app_iterable.close() 267 except: 268 # @@: Print to wsgi.errors? 269 pass 270 new_app_iterable = self.error_callback_app( 271 self.environ, self.start_response, sys.exc_info()) 272 app_iter = iter(new_app_iterable) 273 if hasattr(new_app_iterable, 'close'): 274 self.close = new_app_iterable.close 275 self.next = app_iter.next 276 return self.next() 277 278 def raw_interactive(application, path='', raise_on_wsgi_error=False, 279 **environ): 280 """ 281 Runs the application in a fake environment. 282 """ 283 assert "path_info" not in environ, "argument list changed" 284 if raise_on_wsgi_error: 285 errors = ErrorRaiser() 286 else: 287 errors = six.BytesIO() 288 basic_environ = { 289 # mandatory CGI variables 290 'REQUEST_METHOD': 'GET', # always mandatory 291 'SCRIPT_NAME': '', # may be empty if app is at the root 292 'PATH_INFO': '', # may be empty if at root of app 293 'SERVER_NAME': 'localhost', # always mandatory 294 'SERVER_PORT': '80', # always mandatory 295 'SERVER_PROTOCOL': 'HTTP/1.0', 296 # mandatory wsgi variables 297 'wsgi.version': (1, 0), 298 'wsgi.url_scheme': 'http', 299 'wsgi.input': six.BytesIO(), 300 'wsgi.errors': errors, 301 'wsgi.multithread': False, 302 'wsgi.multiprocess': False, 303 'wsgi.run_once': False, 304 } 305 if path: 306 (_, _, path_info, query, fragment) = urlsplit(str(path)) 307 path_info = unquote(path_info) 308 # urlsplit returns unicode so coerce it back to str 309 path_info, query = str(path_info), str(query) 310 basic_environ['PATH_INFO'] = path_info 311 if query: 312 basic_environ['QUERY_STRING'] = query 313 for name, value in environ.items(): 314 name = name.replace('__', '.') 315 basic_environ[name] = value 316 if ('SERVER_NAME' in basic_environ 317 and 'HTTP_HOST' not in basic_environ): 318 basic_environ['HTTP_HOST'] = basic_environ['SERVER_NAME'] 319 istream = basic_environ['wsgi.input'] 320 if isinstance(istream, bytes): 321 basic_environ['wsgi.input'] = six.BytesIO(istream) 322 basic_environ['CONTENT_LENGTH'] = len(istream) 323 data = {} 324 output = [] 325 headers_set = [] 326 headers_sent = [] 327 def start_response(status, headers, exc_info=None): 328 if exc_info: 329 try: 330 if headers_sent: 331 # Re-raise original exception only if headers sent 332 six.reraise(exc_info[0], exc_info[1], exc_info[2]) 333 finally: 334 # avoid dangling circular reference 335 exc_info = None 336 elif headers_set: 337 # You cannot set the headers more than once, unless the 338 # exc_info is provided. 339 raise AssertionError("Headers already set and no exc_info!") 340 headers_set.append(True) 341 data['status'] = status 342 data['headers'] = headers 343 return output.append 344 app_iter = application(basic_environ, start_response) 345 try: 346 try: 347 for s in app_iter: 348 if not isinstance(s, six.binary_type): 349 raise ValueError( 350 "The app_iter response can only contain bytes (not " 351 "unicode); got: %r" % s) 352 headers_sent.append(True) 353 if not headers_set: 354 raise AssertionError("Content sent w/o headers!") 355 output.append(s) 356 except TypeError as e: 357 # Typically "iteration over non-sequence", so we want 358 # to give better debugging information... 359 e.args = ((e.args[0] + ' iterable: %r' % app_iter),) + e.args[1:] 360 raise 361 finally: 362 if hasattr(app_iter, 'close'): 363 app_iter.close() 364 return (data['status'], data['headers'], b''.join(output), 365 errors.getvalue()) 366 367 class ErrorRaiser(object): 368 369 def flush(self): 370 pass 371 372 def write(self, value): 373 if not value: 374 return 375 raise AssertionError( 376 "No errors should be written (got: %r)" % value) 377 378 def writelines(self, seq): 379 raise AssertionError( 380 "No errors should be written (got lines: %s)" % list(seq)) 381 382 def getvalue(self): 383 return '' 384 385 def interactive(*args, **kw): 386 """ 387 Runs the application interatively, wrapping `raw_interactive` but 388 returning the output in a formatted way. 389 """ 390 status, headers, content, errors = raw_interactive(*args, **kw) 391 full = StringIO() 392 if errors: 393 full.write('Errors:\n') 394 full.write(errors.strip()) 395 full.write('\n----------end errors\n') 396 full.write(status + '\n') 397 for name, value in headers: 398 full.write('%s: %s\n' % (name, value)) 399 full.write('\n') 400 full.write(content) 401 return full.getvalue() 402 interactive.proxy = 'raw_interactive' 403 404 def dump_environ(environ, start_response): 405 """ 406 Application which simply dumps the current environment 407 variables out as a plain text response. 408 """ 409 output = [] 410 keys = list(environ.keys()) 411 keys.sort() 412 for k in keys: 413 v = str(environ[k]).replace("\n","\n ") 414 output.append("%s: %s\n" % (k, v)) 415 output.append("\n") 416 content_length = environ.get("CONTENT_LENGTH", '') 417 if content_length: 418 output.append(environ['wsgi.input'].read(int(content_length))) 419 output.append("\n") 420 output = "".join(output) 421 if six.PY3: 422 output = output.encode('utf8') 423 headers = [('Content-Type', 'text/plain'), 424 ('Content-Length', str(len(output)))] 425 start_response("200 OK", headers) 426 return [output] 427 428 def send_file(filename): 429 warnings.warn( 430 "wsgilib.send_file has been moved to paste.fileapp.FileApp", 431 DeprecationWarning, 2) 432 from paste import fileapp 433 return fileapp.FileApp(filename) 434 435 def capture_output(environ, start_response, application): 436 """ 437 Runs application with environ and start_response, and captures 438 status, headers, and body. 439 440 Sends status and header, but *not* body. Returns (status, 441 headers, body). Typically this is used like: 442 443 .. code-block:: python 444 445 def dehtmlifying_middleware(application): 446 def replacement_app(environ, start_response): 447 status, headers, body = capture_output( 448 environ, start_response, application) 449 content_type = header_value(headers, 'content-type') 450 if (not content_type 451 or not content_type.startswith('text/html')): 452 return [body] 453 body = re.sub(r'<.*?>', '', body) 454 return [body] 455 return replacement_app 456 457 """ 458 warnings.warn( 459 'wsgilib.capture_output has been deprecated in favor ' 460 'of wsgilib.intercept_output', 461 DeprecationWarning, 2) 462 data = [] 463 output = StringIO() 464 def replacement_start_response(status, headers, exc_info=None): 465 if data: 466 data[:] = [] 467 data.append(status) 468 data.append(headers) 469 start_response(status, headers, exc_info) 470 return output.write 471 app_iter = application(environ, replacement_start_response) 472 try: 473 for item in app_iter: 474 output.write(item) 475 finally: 476 if hasattr(app_iter, 'close'): 477 app_iter.close() 478 if not data: 479 data.append(None) 480 if len(data) < 2: 481 data.append(None) 482 data.append(output.getvalue()) 483 return data 484 485 def intercept_output(environ, application, conditional=None, 486 start_response=None): 487 """ 488 Runs application with environ and captures status, headers, and 489 body. None are sent on; you must send them on yourself (unlike 490 ``capture_output``) 491 492 Typically this is used like: 493 494 .. code-block:: python 495 496 def dehtmlifying_middleware(application): 497 def replacement_app(environ, start_response): 498 status, headers, body = intercept_output( 499 environ, application) 500 start_response(status, headers) 501 content_type = header_value(headers, 'content-type') 502 if (not content_type 503 or not content_type.startswith('text/html')): 504 return [body] 505 body = re.sub(r'<.*?>', '', body) 506 return [body] 507 return replacement_app 508 509 A third optional argument ``conditional`` should be a function 510 that takes ``conditional(status, headers)`` and returns False if 511 the request should not be intercepted. In that case 512 ``start_response`` will be called and ``(None, None, app_iter)`` 513 will be returned. You must detect that in your code and return 514 the app_iter, like: 515 516 .. code-block:: python 517 518 def dehtmlifying_middleware(application): 519 def replacement_app(environ, start_response): 520 status, headers, body = intercept_output( 521 environ, application, 522 lambda s, h: header_value(headers, 'content-type').startswith('text/html'), 523 start_response) 524 if status is None: 525 return body 526 start_response(status, headers) 527 body = re.sub(r'<.*?>', '', body) 528 return [body] 529 return replacement_app 530 """ 531 if conditional is not None and start_response is None: 532 raise TypeError( 533 "If you provide conditional you must also provide " 534 "start_response") 535 data = [] 536 output = StringIO() 537 def replacement_start_response(status, headers, exc_info=None): 538 if conditional is not None and not conditional(status, headers): 539 data.append(None) 540 return start_response(status, headers, exc_info) 541 if data: 542 data[:] = [] 543 data.append(status) 544 data.append(headers) 545 return output.write 546 app_iter = application(environ, replacement_start_response) 547 if data[0] is None: 548 return (None, None, app_iter) 549 try: 550 for item in app_iter: 551 output.write(item) 552 finally: 553 if hasattr(app_iter, 'close'): 554 app_iter.close() 555 if not data: 556 data.append(None) 557 if len(data) < 2: 558 data.append(None) 559 data.append(output.getvalue()) 560 return data 561 562 ## Deprecation warning wrapper: 563 564 class ResponseHeaderDict(HeaderDict): 565 566 def __init__(self, *args, **kw): 567 warnings.warn( 568 "The class wsgilib.ResponseHeaderDict has been moved " 569 "to paste.response.HeaderDict", 570 DeprecationWarning, 2) 571 HeaderDict.__init__(self, *args, **kw) 572 573 def _warn_deprecated(new_func): 574 new_name = new_func.func_name 575 new_path = new_func.func_globals['__name__'] + '.' + new_name 576 def replacement(*args, **kw): 577 warnings.warn( 578 "The function wsgilib.%s has been moved to %s" 579 % (new_name, new_path), 580 DeprecationWarning, 2) 581 return new_func(*args, **kw) 582 try: 583 replacement.func_name = new_func.func_name 584 except: 585 pass 586 return replacement 587 588 # Put warnings wrapper in place for all public functions that 589 # were imported from elsewhere: 590 591 for _name in __all__: 592 _func = globals()[_name] 593 if (hasattr(_func, 'func_globals') 594 and _func.func_globals['__name__'] != __name__): 595 globals()[_name] = _warn_deprecated(_func) 596 597 if __name__ == '__main__': 598 import doctest 599 doctest.testmod() 600 601