wsgilib.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597
  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. # functions which moved to paste.request and paste.response
  7. # Deprecated around 15 Dec 2005
  8. from paste.request import get_cookies, parse_querystring, parse_formvars
  9. from paste.request import construct_url, path_info_split, path_info_pop
  10. from paste.response import HeaderDict, has_header, header_value, remove_header
  11. from paste.response import error_body_response, error_response, error_response_app
  12. from traceback import print_exception
  13. import urllib
  14. from cStringIO import StringIO
  15. import sys
  16. from urlparse import urlsplit
  17. import warnings
  18. __all__ = ['add_close', 'add_start_close', 'capture_output', 'catch_errors',
  19. 'catch_errors_app', 'chained_app_iters', 'construct_url',
  20. 'dump_environ', 'encode_unicode_app_iter', 'error_body_response',
  21. 'error_response', 'get_cookies', 'has_header', 'header_value',
  22. 'interactive', 'intercept_output', 'path_info_pop',
  23. 'path_info_split', 'raw_interactive', 'send_file']
  24. class add_close(object):
  25. """
  26. An an iterable that iterates over app_iter, then calls
  27. close_func.
  28. """
  29. def __init__(self, app_iterable, close_func):
  30. self.app_iterable = app_iterable
  31. self.app_iter = iter(app_iterable)
  32. self.close_func = close_func
  33. self._closed = False
  34. def __iter__(self):
  35. return self
  36. def next(self):
  37. return self.app_iter.next()
  38. def close(self):
  39. self._closed = True
  40. if hasattr(self.app_iterable, 'close'):
  41. self.app_iterable.close()
  42. self.close_func()
  43. def __del__(self):
  44. if not self._closed:
  45. # We can't raise an error or anything at this stage
  46. print >> sys.stderr, (
  47. "Error: app_iter.close() was not called when finishing "
  48. "WSGI request. finalization function %s not called"
  49. % self.close_func)
  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 self.app_iter.next()
  70. def close(self):
  71. self._closed = True
  72. if hasattr(self.app_iterable, 'close'):
  73. self.app_iterable.close()
  74. if self.close_func is not None:
  75. self.close_func()
  76. def __del__(self):
  77. if not self._closed:
  78. # We can't raise an error or anything at this stage
  79. print >> sys.stderr, (
  80. "Error: app_iter.close() was not called when finishing "
  81. "WSGI request. finalization function %s not called"
  82. % self.close_func)
  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. raise 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 >> sys.stderr, (
  118. "Error: app_iter.close() was not called when finishing "
  119. "WSGI request. finalization function %s not called"
  120. % self.close_func)
  121. class encode_unicode_app_iter(object):
  122. """
  123. Encodes an app_iterable's unicode responses as strings
  124. """
  125. def __init__(self, app_iterable, encoding=sys.getdefaultencoding(),
  126. errors='strict'):
  127. self.app_iterable = app_iterable
  128. self.app_iter = iter(app_iterable)
  129. self.encoding = encoding
  130. self.errors = errors
  131. def __iter__(self):
  132. return self
  133. def next(self):
  134. content = self.app_iter.next()
  135. if isinstance(content, unicode):
  136. content = content.encode(self.encoding, self.errors)
  137. return content
  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 = StringIO()
  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': StringIO(''),
  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 = urllib.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, str):
  281. basic_environ['wsgi.input'] = StringIO(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. raise 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, str):
  309. raise ValueError(
  310. "The app_iter response can only contain str (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, 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'], ''.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 = 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. headers = [('Content-Type', 'text/plain'),
  375. ('Content-Length', str(len(output)))]
  376. start_response("200 OK", headers)
  377. return [output]
  378. def send_file(filename):
  379. warnings.warn(
  380. "wsgilib.send_file has been moved to paste.fileapp.FileApp",
  381. DeprecationWarning, 2)
  382. from paste import fileapp
  383. return fileapp.FileApp(filename)
  384. def capture_output(environ, start_response, application):
  385. """
  386. Runs application with environ and start_response, and captures
  387. status, headers, and body.
  388. Sends status and header, but *not* body. Returns (status,
  389. headers, body). Typically this is used like:
  390. .. code-block:: python
  391. def dehtmlifying_middleware(application):
  392. def replacement_app(environ, start_response):
  393. status, headers, body = capture_output(
  394. environ, start_response, application)
  395. content_type = header_value(headers, 'content-type')
  396. if (not content_type
  397. or not content_type.startswith('text/html')):
  398. return [body]
  399. body = re.sub(r'<.*?>', '', body)
  400. return [body]
  401. return replacement_app
  402. """
  403. warnings.warn(
  404. 'wsgilib.capture_output has been deprecated in favor '
  405. 'of wsgilib.intercept_output',
  406. DeprecationWarning, 2)
  407. data = []
  408. output = StringIO()
  409. def replacement_start_response(status, headers, exc_info=None):
  410. if data:
  411. data[:] = []
  412. data.append(status)
  413. data.append(headers)
  414. start_response(status, headers, exc_info)
  415. return output.write
  416. app_iter = application(environ, replacement_start_response)
  417. try:
  418. for item in app_iter:
  419. output.write(item)
  420. finally:
  421. if hasattr(app_iter, 'close'):
  422. app_iter.close()
  423. if not data:
  424. data.append(None)
  425. if len(data) < 2:
  426. data.append(None)
  427. data.append(output.getvalue())
  428. return data
  429. def intercept_output(environ, application, conditional=None,
  430. start_response=None):
  431. """
  432. Runs application with environ and captures status, headers, and
  433. body. None are sent on; you must send them on yourself (unlike
  434. ``capture_output``)
  435. Typically this is used like:
  436. .. code-block:: python
  437. def dehtmlifying_middleware(application):
  438. def replacement_app(environ, start_response):
  439. status, headers, body = intercept_output(
  440. environ, application)
  441. start_response(status, headers)
  442. content_type = header_value(headers, 'content-type')
  443. if (not content_type
  444. or not content_type.startswith('text/html')):
  445. return [body]
  446. body = re.sub(r'<.*?>', '', body)
  447. return [body]
  448. return replacement_app
  449. A third optional argument ``conditional`` should be a function
  450. that takes ``conditional(status, headers)`` and returns False if
  451. the request should not be intercepted. In that case
  452. ``start_response`` will be called and ``(None, None, app_iter)``
  453. will be returned. You must detect that in your code and return
  454. the app_iter, like:
  455. .. code-block:: python
  456. def dehtmlifying_middleware(application):
  457. def replacement_app(environ, start_response):
  458. status, headers, body = intercept_output(
  459. environ, application,
  460. lambda s, h: header_value(headers, 'content-type').startswith('text/html'),
  461. start_response)
  462. if status is None:
  463. return body
  464. start_response(status, headers)
  465. body = re.sub(r'<.*?>', '', body)
  466. return [body]
  467. return replacement_app
  468. """
  469. if conditional is not None and start_response is None:
  470. raise TypeError(
  471. "If you provide conditional you must also provide "
  472. "start_response")
  473. data = []
  474. output = StringIO()
  475. def replacement_start_response(status, headers, exc_info=None):
  476. if conditional is not None and not conditional(status, headers):
  477. data.append(None)
  478. return start_response(status, headers, exc_info)
  479. if data:
  480. data[:] = []
  481. data.append(status)
  482. data.append(headers)
  483. return output.write
  484. app_iter = application(environ, replacement_start_response)
  485. if data[0] is None:
  486. return (None, None, app_iter)
  487. try:
  488. for item in app_iter:
  489. output.write(item)
  490. finally:
  491. if hasattr(app_iter, 'close'):
  492. app_iter.close()
  493. if not data:
  494. data.append(None)
  495. if len(data) < 2:
  496. data.append(None)
  497. data.append(output.getvalue())
  498. return data
  499. ## Deprecation warning wrapper:
  500. class ResponseHeaderDict(HeaderDict):
  501. def __init__(self, *args, **kw):
  502. warnings.warn(
  503. "The class wsgilib.ResponseHeaderDict has been moved "
  504. "to paste.response.HeaderDict",
  505. DeprecationWarning, 2)
  506. HeaderDict.__init__(self, *args, **kw)
  507. def _warn_deprecated(new_func):
  508. new_name = new_func.func_name
  509. new_path = new_func.func_globals['__name__'] + '.' + new_name
  510. def replacement(*args, **kw):
  511. warnings.warn(
  512. "The function wsgilib.%s has been moved to %s"
  513. % (new_name, new_path),
  514. DeprecationWarning, 2)
  515. return new_func(*args, **kw)
  516. try:
  517. replacement.func_name = new_func.func_name
  518. except:
  519. pass
  520. return replacement
  521. # Put warnings wrapper in place for all public functions that
  522. # were imported from elsewhere:
  523. for _name in __all__:
  524. _func = globals()[_name]
  525. if (hasattr(_func, 'func_globals')
  526. and _func.func_globals['__name__'] != __name__):
  527. globals()[_name] = _warn_deprecated(_func)
  528. if __name__ == '__main__':
  529. import doctest
  530. doctest.testmod()