wsgilib.py 20 KB

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