Skip to content
Browse files

Merge remote-tracking branch 'upstream/master'

Conflicts:
	tornado/httpserver.py
	tornado/iostream.py
	tornado/platform/common.py
	tornado/testing.py
  • Loading branch information...
2 parents 197e88e + ed7030d commit 5a8b98cc382289c9c130da38dfa84742118ede08 @mgenti committed Jul 2, 2012
Showing with 17,528 additions and 2,368 deletions.
  1. +18 −0 .coveragerc
  2. +12 −3 .gitignore
  3. +33 −0 .travis.yml
  4. +12 −1 MANIFEST.in
  5. +38 −10 README
  6. +1 −0 demos/appengine/blog.py
  7. +6 −0 demos/auth/authdemo.py
  8. +78 −0 demos/benchmark/benchmark.py
  9. +47 −0 demos/benchmark/chunk_benchmark.py
  10. +67 −0 demos/benchmark/template_benchmark.py
  11. +1 −0 demos/blog/blog.py
  12. +15 −8 demos/chat/chatdemo.py
  13. +3 −3 demos/chat/templates/index.html
  14. +1 −1 demos/chat/templates/message.html
  15. +22 −27 demos/facebook/facebook.py
  16. +7 −19 demos/facebook/templates/modules/post.html
  17. +2 −2 demos/facebook/templates/stream.html
  18. +0 −22 demos/facebook/uimodules.py
  19. +2 −1 {tornado → demos/s3server}/s3server.py
  20. +106 −0 demos/websocket/chatdemo.py
  21. +56 −0 demos/websocket/static/chat.css
  22. +72 −0 demos/websocket/static/chat.js
  23. +33 −0 demos/websocket/templates/index.html
  24. +2 −0 demos/websocket/templates/message.html
  25. +3 −0 maint/README
  26. +8 −0 maint/appengine/README
  27. +75 −0 maint/appengine/common/cgi_runtests.py
  28. +53 −0 maint/appengine/common/runtests.py
  29. +8 −0 maint/appengine/py25/app.yaml
  30. +1 −0 maint/appengine/py25/cgi_runtests.py
  31. +1 −0 maint/appengine/py25/runtests.py
  32. +1 −0 maint/appengine/py25/tornado
  33. +9 −0 maint/appengine/py27/app.yaml
  34. +1 −0 maint/appengine/py27/cgi_runtests.py
  35. +1 −0 maint/appengine/py27/runtests.py
  36. +1 −0 maint/appengine/py27/tornado
  37. +4 −0 maint/appengine/setup.py
  38. +19 −0 maint/appengine/tox.ini
  39. +23 −0 maint/requirements.txt
  40. 0 {website/markdown/extensions → maint/scripts/custom_fixers}/__init__.py
  41. +60 −0 maint/scripts/custom_fixers/fix_future_imports.py
  42. +8 −0 maint/scripts/run_autopep8.sh
  43. +6 −0 maint/scripts/run_fixers.py
  44. +3 −0 maint/test/README
  45. +1 −0 maint/test/websocket/.gitignore
  46. +32 −0 maint/test/websocket/client.py
  47. +37 −0 maint/test/websocket/run.sh
  48. +21 −0 maint/test/websocket/server.py
  49. +12 −0 maint/test/websocket/tox.ini
  50. +23 −0 maint/vm/README
  51. +23 −0 maint/vm/freebsd/Vagrantfile
  52. +30 −0 maint/vm/freebsd/setup.sh
  53. +13 −0 maint/vm/freebsd/tox.ini
  54. +9 −0 maint/vm/shared-setup.sh
  55. +9 −0 maint/vm/ubuntu10.04/Vagrantfile
  56. +50 −0 maint/vm/ubuntu10.04/setup.sh
  57. +33 −0 maint/vm/ubuntu10.04/tox.ini
  58. +8 −0 maint/vm/ubuntu12.04/Vagrantfile
  59. +48 −0 maint/vm/ubuntu12.04/setup.sh
  60. +27 −0 maint/vm/ubuntu12.04/tox.ini
  61. +10 −0 runtests.sh
  62. +28 −2 setup.py
  63. +12 −0 tornado/__init__.py
  64. +459 −188 tornado/auth.py
  65. +238 −42 tornado/autoreload.py
  66. +3,576 −0 tornado/ca-certificates.crt
  67. +441 −0 tornado/curl_httpclient.py
  68. +63 −20 tornado/database.py
  69. +248 −20 tornado/escape.py
  70. +409 −0 tornado/gen.py
  71. +256 −446 tornado/httpclient.py
  72. +284 −282 tornado/httpserver.py
  73. +176 −8 tornado/httputil.py
  74. +282 −131 tornado/ioloop.py
  75. +590 −109 tornado/iostream.py
  76. +93 −37 tornado/locale.py
  77. +343 −0 tornado/netutil.py
  78. +242 −147 tornado/options.py
  79. 0 tornado/platform/__init__.py
  80. +34 −0 tornado/platform/auto.py
  81. +32 −70 tornado/{win32_support.py → platform/common.py}
  82. +59 −0 tornado/platform/interface.py
  83. +68 −0 tornado/platform/posix.py
  84. +330 −0 tornado/platform/twisted.py
  85. +20 −0 tornado/platform/windows.py
  86. +158 −0 tornado/process.py
  87. +534 −0 tornado/simple_httpclient.py
  88. +201 −62 tornado/stack_context.py
  89. +376 −97 tornado/template.py
  90. +200 −0 tornado/test/auth_test.py
  91. +1 −0 tornado/test/csv_translations/fr_FR.csv
  92. +25 −0 tornado/test/curl_httpclient_test.py
  93. +199 −0 tornado/test/escape_test.py
  94. +351 −0 tornado/test/gen_test.py
  95. +9 −0 tornado/test/gettext_translations/extract_me.py
  96. BIN tornado/test/gettext_translations/fr_FR/LC_MESSAGES/tornado_test.mo
  97. +22 −0 tornado/test/gettext_translations/fr_FR/LC_MESSAGES/tornado_test.po
  98. +191 −0 tornado/test/httpclient_test.py
  99. +383 −30 tornado/test/httpserver_test.py
  100. +222 −0 tornado/test/httputil_test.py
  101. +59 −0 tornado/test/import_test.py
  102. +28 −3 tornado/test/ioloop_test.py
  103. +253 −0 tornado/test/iostream_test.py
  104. +39 −0 tornado/test/locale_test.py
  105. +102 −0 tornado/test/options_test.py
  106. +130 −0 tornado/test/process_test.py
  107. +39 −0 tornado/test/runtests.py
  108. +321 −0 tornado/test/simple_httpclient_test.py
  109. +52 −15 tornado/test/stack_context_test.py
  110. +2 −0 tornado/test/static/robots.txt
  111. +365 −0 tornado/test/template_test.py
  112. +1 −0 tornado/test/templates/utf8.html
  113. +15 −0 tornado/test/test.crt
  114. +16 −0 tornado/test/test.key
  115. +45 −2 tornado/test/testing_test.py
  116. +530 −0 tornado/test/twisted_test.py
  117. +26 −0 tornado/test/util_test.py
  118. +851 −0 tornado/test/web_test.py
  119. +82 −0 tornado/test/wsgi_test.py
  120. +194 −73 tornado/testing.py
  121. +101 −0 tornado/util.py
  122. +901 −376 tornado/web.py
  123. +654 −0 tornado/websocket.py
  124. +127 −110 tornado/wsgi.py
  125. +111 −0 tox.ini
  126. +13 −0 website/Makefile
  127. +11 −1 website/app.yaml
Sorry, we could not display the entire diff because it was too big.
View
18 .coveragerc
@@ -0,0 +1,18 @@
+# Test coverage configuration.
+# Usage:
+# pip install coverage
+# coverage erase # clears previous data if any
+# coverage run -m tornado.test.runtests
+# coverage report # prints to stdout
+# coverage html # creates ./htmlcov/*.html including annotated source
+[run]
+branch = true
+source = tornado
+omit =
+ tornado/platform/*
+ tornado/test/*
+ */_auto2to3*
+
+[report]
+# Ignore missing source files, i.e. fake template-generated "files"
+ignore_errors = true
View
15 .gitignore
@@ -1,6 +1,15 @@
*.pyc
+*.pyo
*.so
+*.class
*~
-build
-dist/
-tornado.egg-info
+build/
+/dist/
+MANIFEST
+/tornado.egg-info/
+_auto2to3*
+.tox/
+.vagrant
+/.coverage
+/htmlcov/
+/env/
View
33 .travis.yml
@@ -0,0 +1,33 @@
+# http://travis-ci.org/#!/facebook/tornado
+language: python
+python:
+ - 2.5
+ - 2.6
+ - 2.7
+ - pypy
+ - 3.2
+matrix:
+ include:
+ - python: 2.5
+ env: FULL="true"
+ - python: 2.6
+ env: FULL="true"
+ - python: 2.7
+ env: FULL="true"
+ - python: 3.2
+ env: LANG="en_US.utf-8"
+ - python: 3.2
+ env: LANG="C"
+install:
+ - if [[ $TRAVIS_PYTHON_VERSION == '2.5' ]]; then pip install --use-mirrors simplejson; fi
+ - if [[ $FULL == 'true' ]]; then sudo apt-get install librtmp-dev; pip install --use-mirrors MySQL-python pycurl; fi
+ - if [[ $FULL == 'true' && $TRAVIS_PYTHON_VERSION == '2.5' ]]; then pip install --use-mirrors twisted==11.0.0 'zope.interface<4.0'; fi
+ - if [[ $FULL == 'true' && $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --use-mirrors twisted==11.0.0; fi
+ - if [[ $FULL == 'true' && $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install --use-mirrors twisted==12.0.0; fi
+ - python setup.py install
+script:
+ # Must cd somewhere else so python3 doesn't get confused and run
+ # the python2 code from the current directory instead of the installed
+ # 2to3 version.
+ - cd maint
+ - python -m tornado.test.runtests
View
13 MANIFEST.in
@@ -1,2 +1,13 @@
-recursive-include demos *.py *.yaml *.html *.css *.png *.js *.xml *.sql README
+recursive-include demos *.py *.yaml *.html *.css *.js *.xml *.sql README
include tornado/epoll.c
+include tornado/ca-certificates.crt
+include tornado/test/README
+include tornado/test/test.crt
+include tornado/test/test.key
+include tornado/test/csv_translations/fr_FR.csv
+include tornado/test/gettext_translations/fr_FR/LC_MESSAGES/tornado_test.mo
+include tornado/test/gettext_translations/fr_FR/LC_MESSAGES/tornado_test.po
+include tornado/test/static/robots.txt
+include tornado/test/templates/utf8.html
+include runtests.sh
+global-exclude _auto2to3*
View
48 README
@@ -7,21 +7,49 @@ available at http://www.tornadoweb.org/
Tornado is licensed under the Apache Licence, Version 2.0
(http://www.apache.org/licenses/LICENSE-2.0.html).
-Installation
-============
-To install:
+Automatic installation
+----------------------
+ Tornado is listed in PyPI and can be installed with pip or
+easy_install. Note that the source distribution includes demo
+applications that are not present when Tornado is installed in this
+way, so you may wish to download a copy of the source tarball as well.
+
+Manual installation
+-------------------
+
+Download https://github.com/downloads/facebook/tornado/tornado-2.3.tar.gz
+
+ tar xvzf tornado-2.3.tar.gz
+ cd tornado-2.3
python setup.py build
sudo python setup.py install
-Tornado has been tested on Python 2.5 and 2.6. To use all of the features
-of Tornado, you need to have PycURL and a JSON library like simplejson
-installed.
+The Tornado source code is hosted on GitHub: https://github.com/facebook/tornado
+
+On Python 2.6 and 2.7, it is also possible to simply add the tornado
+directory to your PYTHONPATH instead of building with setup.py, since
+the standard library includes epoll support.
+
+Prerequisites
+-------------
+
+Tornado runs on Python 2.5, 2.6, 2.7 and 3.2.
+
+On Python 2.6 and 2.7, there are no dependencies outside the Python
+standard library, although PycURL (version 7.18.2 or higher required;
+version 7.21.1 or higher recommended) may be used if desired.
-On Mac OS X, you can install the packages with:
+On Python 2.5, PycURL is required, along with simplejson and the
+Python development headers (typically obtained by installing a package
+named something like python-dev from your operating system).
- sudo easy_install setuptools pycurl==7.16.2.1 simplejson
+On Python 3.2, the distribute package is required. Note that Python 3
+support is relatively new and may have bugs.
-On Ubuntu Linux, you can install the packages with:
+Platforms
+---------
- sudo apt-get install python-pycurl python-simplejson
+ Tornado should run on any Unix-like platform, although for the best
+performance and scalability only Linux and BSD (including BSD
+derivatives like Mac OS X) are recommended.
View
1 demos/appengine/blog.py
@@ -151,6 +151,7 @@ def render(self, entry):
"template_path": os.path.join(os.path.dirname(__file__), "templates"),
"ui_modules": {"Entry": EntryModule},
"xsrf_cookies": True,
+ "autoescape": None,
}
application = tornado.wsgi.WSGIApplication([
(r"/", HomeHandler),
View
6 demos/auth/authdemo.py
@@ -31,6 +31,7 @@ def __init__(self):
handlers = [
(r"/", MainHandler),
(r"/auth/login", AuthHandler),
+ (r"/auth/logout", LogoutHandler),
]
settings = dict(
cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
@@ -51,6 +52,7 @@ class MainHandler(BaseHandler):
def get(self):
name = tornado.escape.xhtml_escape(self.current_user["name"])
self.write("Hello, " + name)
+ self.write("<br><br><a href=\"/auth/logout\">Log out</a>")
class AuthHandler(BaseHandler, tornado.auth.GoogleMixin):
@@ -67,6 +69,10 @@ def _on_auth(self, user):
self.set_secure_cookie("user", tornado.escape.json_encode(user))
self.redirect("/")
+class LogoutHandler(BaseHandler):
+ def get(self):
+ self.clear_cookie("user")
+ self.redirect("/")
def main():
tornado.options.parse_command_line()
View
78 demos/benchmark/benchmark.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python
+#
+# A simple benchmark of tornado's HTTP stack.
+# Requires 'ab' to be installed.
+#
+# Running without profiling:
+# demos/benchmark/benchmark.py
+# demos/benchmark/benchmark.py --quiet --num_runs=5|grep "Requests per second"
+#
+# Running with profiling:
+#
+# python -m cProfile -o /tmp/prof demos/benchmark/benchmark.py
+# python -m pstats /tmp/prof
+# % sort time
+# % stats 20
+
+from tornado.ioloop import IOLoop
+from tornado.options import define, options, parse_command_line
+from tornado.web import RequestHandler, Application
+
+import random
+import signal
+import subprocess
+
+# choose a random port to avoid colliding with TIME_WAIT sockets left over
+# from previous runs.
+define("min_port", type=int, default=8000)
+define("max_port", type=int, default=9000)
+
+# Increasing --n without --keepalive will eventually run into problems
+# due to TIME_WAIT sockets
+define("n", type=int, default=15000)
+define("c", type=int, default=25)
+define("keepalive", type=bool, default=False)
+define("quiet", type=bool, default=False)
+
+# Repeat the entire benchmark this many times (on different ports)
+# This gives JITs time to warm up, etc. Pypy needs 3-5 runs at
+# --n=15000 for its JIT to reach full effectiveness
+define("num_runs", type=int, default=1)
+
+class RootHandler(RequestHandler):
+ def get(self):
+ self.write("Hello, world")
+
+ def _log(self):
+ pass
+
+def handle_sigchld(sig, frame):
+ IOLoop.instance().add_callback(IOLoop.instance().stop)
+
+def main():
+ parse_command_line()
+ for i in xrange(options.num_runs):
+ run()
+
+def run():
+ app = Application([("/", RootHandler)])
+ port = random.randrange(options.min_port, options.max_port)
+ app.listen(port, address='127.0.0.1')
+ signal.signal(signal.SIGCHLD, handle_sigchld)
+ args = ["ab"]
+ args.extend(["-n", str(options.n)])
+ args.extend(["-c", str(options.c)])
+ if options.keepalive:
+ args.append("-k")
+ if options.quiet:
+ # just stops the progress messages printed to stderr
+ args.append("-q")
+ args.append("http://127.0.0.1:%d/" % port)
+ subprocess.Popen(args)
+ IOLoop.instance().start()
+ IOLoop.instance().close()
+ del IOLoop._instance
+ assert not IOLoop.initialized()
+
+if __name__ == '__main__':
+ main()
View
47 demos/benchmark/chunk_benchmark.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+#
+# Downloads a large file in chunked encoding with both curl and simple clients
+
+import logging
+from tornado.curl_httpclient import CurlAsyncHTTPClient
+from tornado.simple_httpclient import SimpleAsyncHTTPClient
+from tornado.ioloop import IOLoop
+from tornado.options import define, options, parse_command_line
+from tornado.web import RequestHandler, Application
+
+define('port', default=8888)
+define('num_chunks', default=1000)
+define('chunk_size', default=2048)
+
+class ChunkHandler(RequestHandler):
+ def get(self):
+ for i in xrange(options.num_chunks):
+ self.write('A' * options.chunk_size)
+ self.flush()
+ self.finish()
+
+def main():
+ parse_command_line()
+ app = Application([('/', ChunkHandler)])
+ app.listen(options.port, address='127.0.0.1')
+ def callback(response):
+ response.rethrow()
+ assert len(response.body) == (options.num_chunks * options.chunk_size)
+ logging.warning("fetch completed in %s seconds", response.request_time)
+ IOLoop.instance().stop()
+
+ logging.warning("Starting fetch with curl client")
+ curl_client = CurlAsyncHTTPClient()
+ curl_client.fetch('http://localhost:%d/' % options.port,
+ callback=callback)
+ IOLoop.instance().start()
+
+ logging.warning("Starting fetch with simple client")
+ simple_client = SimpleAsyncHTTPClient()
+ simple_client.fetch('http://localhost:%d/' % options.port,
+ callback=callback)
+ IOLoop.instance().start()
+
+
+if __name__ == '__main__':
+ main()
View
67 demos/benchmark/template_benchmark.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+#
+# A simple benchmark of tornado template rendering, based on
+# https://github.com/mitsuhiko/jinja2/blob/master/examples/bench.py
+
+import sys
+from timeit import Timer
+
+from tornado.options import options, define, parse_command_line
+from tornado.template import Template
+
+define('num', default=100, help='number of iterations')
+define('dump', default=False, help='print template generated code and exit')
+
+context = {
+ 'page_title': 'mitsuhiko\'s benchmark',
+ 'table': [dict(a=1,b=2,c=3,d=4,e=5,f=6,g=7,h=8,i=9,j=10) for x in range(1000)]
+}
+
+tmpl = Template("""\
+<!doctype html>
+<html>
+ <head>
+ <title>{{ page_title }}</title>
+ </head>
+ <body>
+ <div class="header">
+ <h1>{{ page_title }}</h1>
+ </div>
+ <ul class="navigation">
+ {% for href, caption in [ \
+ ('index.html', 'Index'), \
+ ('downloads.html', 'Downloads'), \
+ ('products.html', 'Products') \
+ ] %}
+ <li><a href="{{ href }}">{{ caption }}</a></li>
+ {% end %}
+ </ul>
+ <div class="table">
+ <table>
+ {% for row in table %}
+ <tr>
+ {% for cell in row %}
+ <td>{{ cell }}</td>
+ {% end %}
+ </tr>
+ {% end %}
+ </table>
+ </div>
+ </body>
+</html>\
+""")
+
+def render():
+ tmpl.generate(**context)
+
+def main():
+ parse_command_line()
+ if options.dump:
+ print tmpl.code
+ sys.exit(0)
+ t = Timer(render)
+ results = t.timeit(options.num) / options.num
+ print '%0.3f ms per iteration' % (results*1000)
+
+if __name__ == '__main__':
+ main()
View
1 demos/blog/blog.py
@@ -53,6 +53,7 @@ def __init__(self):
xsrf_cookies=True,
cookie_secret="11oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
login_url="/auth/login",
+ autoescape=None,
)
tornado.web.Application.__init__(self, handlers, **settings)
View
23 demos/chat/chatdemo.py
@@ -17,7 +17,6 @@
import logging
import tornado.auth
import tornado.escape
-import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
@@ -44,6 +43,7 @@ def __init__(self):
template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),
xsrf_cookies=True,
+ autoescape="xhtml_escape",
)
tornado.web.Application.__init__(self, handlers, **settings)
@@ -62,7 +62,7 @@ def get(self):
class MessageMixin(object):
- waiters = []
+ waiters = set()
cache = []
cache_size = 200
@@ -77,7 +77,11 @@ def wait_for_messages(self, callback, cursor=None):
if recent:
callback(recent)
return
- cls.waiters.append(callback)
+ cls.waiters.add(callback)
+
+ def cancel_wait(self, callback):
+ cls = MessageMixin
+ cls.waiters.remove(callback)
def new_messages(self, messages):
cls = MessageMixin
@@ -87,7 +91,7 @@ def new_messages(self, messages):
callback(messages)
except:
logging.error("Error in waiter callback", exc_info=True)
- cls.waiters = []
+ cls.waiters = set()
cls.cache.extend(messages)
if len(cls.cache) > self.cache_size:
cls.cache = cls.cache[-self.cache_size:]
@@ -114,7 +118,7 @@ class MessageUpdatesHandler(BaseHandler, MessageMixin):
@tornado.web.asynchronous
def post(self):
cursor = self.get_argument("cursor", None)
- self.wait_for_messages(self.async_callback(self.on_new_messages),
+ self.wait_for_messages(self.on_new_messages,
cursor=cursor)
def on_new_messages(self, messages):
@@ -123,6 +127,9 @@ def on_new_messages(self, messages):
return
self.finish(dict(messages=messages))
+ def on_connection_close(self):
+ self.cancel_wait(self.on_new_messages)
+
class AuthLoginHandler(BaseHandler, tornado.auth.GoogleMixin):
@tornado.web.asynchronous
@@ -131,7 +138,7 @@ def get(self):
self.get_authenticated_user(self.async_callback(self._on_auth))
return
self.authenticate_redirect(ax_attrs=["name"])
-
+
def _on_auth(self, user):
if not user:
raise tornado.web.HTTPError(500, "Google auth failed")
@@ -147,8 +154,8 @@ def get(self):
def main():
tornado.options.parse_command_line()
- http_server = tornado.httpserver.HTTPServer(Application())
- http_server.listen(options.port)
+ app = Application()
+ app.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
View
6 demos/chat/templates/index.html
@@ -7,13 +7,13 @@
</head>
<body>
<div id="nav">
- <b>{{ escape(current_user["name"]) }}</b> -
+ <b>{{ current_user["name"] }}</b> -
<a href="/auth/logout">{{ _("Sign out") }}</a>
</div>
<div id="body">
<div id="inbox">
{% for message in messages %}
- {% include "message.html" %}
+ {% module Template("message.html", message=message) %}
{% end %}
</div>
<div id="input">
@@ -24,7 +24,7 @@
<td style="padding-left:5px">
<input type="submit" value="{{ _("Post") }}"/>
<input type="hidden" name="next" value="{{ request.path }}"/>
- {{ xsrf_form_html() }}
+ {% module xsrf_form_html() %}
</td>
</tr>
</table>
View
2 demos/chat/templates/message.html
@@ -1 +1 @@
-<div class="message" id="m{{ message["id"] }}"><b>{{ escape(message["from"]) }}: </b>{{ escape(message["body"]) }}</div>
+<div class="message" id="m{{ message["id"] }}"><b>{{ message["from"] }}: </b>{% module linkify(message["body"]) %}</div>
View
49 demos/facebook/facebook.py
@@ -14,15 +14,13 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
import os.path
import tornado.auth
import tornado.escape
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
-import uimodules
from tornado.options import define, options
@@ -48,8 +46,9 @@ def __init__(self):
xsrf_cookies=True,
facebook_api_key=options.facebook_api_key,
facebook_secret=options.facebook_secret,
- ui_modules= {"Post": PostModule},
+ ui_modules={"Post": PostModule},
debug=True,
+ autoescape=None,
)
tornado.web.Application.__init__(self, handlers, **settings)
@@ -61,32 +60,38 @@ def get_current_user(self):
return tornado.escape.json_decode(user_json)
-class MainHandler(BaseHandler, tornado.auth.FacebookMixin):
+class MainHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
@tornado.web.authenticated
@tornado.web.asynchronous
def get(self):
- self.facebook_request(
- method="stream.get",
- callback=self.async_callback(self._on_stream),
- session_key=self.current_user["session_key"])
+ self.facebook_request("/me/home", self._on_stream,
+ access_token=self.current_user["access_token"])
def _on_stream(self, stream):
if stream is None:
# Session may have expired
self.redirect("/auth/login")
return
- # Turn profiles into a dict mapping id => profile
- stream["profiles"] = dict((p["id"], p) for p in stream["profiles"])
self.render("stream.html", stream=stream)
-class AuthLoginHandler(BaseHandler, tornado.auth.FacebookMixin):
+class AuthLoginHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
@tornado.web.asynchronous
def get(self):
- if self.get_argument("session", None):
- self.get_authenticated_user(self.async_callback(self._on_auth))
+ my_url = (self.request.protocol + "://" + self.request.host +
+ "/auth/login?next=" +
+ tornado.escape.url_escape(self.get_argument("next", "/")))
+ if self.get_argument("code", False):
+ self.get_authenticated_user(
+ redirect_uri=my_url,
+ client_id=self.settings["facebook_api_key"],
+ client_secret=self.settings["facebook_secret"],
+ code=self.get_argument("code"),
+ callback=self._on_auth)
return
- self.authorize_redirect("read_stream")
+ self.authorize_redirect(redirect_uri=my_url,
+ client_id=self.settings["facebook_api_key"],
+ extra_params={"scope": "read_stream"})
def _on_auth(self, user):
if not user:
@@ -95,25 +100,15 @@ def _on_auth(self, user):
self.redirect(self.get_argument("next", "/"))
-class AuthLogoutHandler(BaseHandler, tornado.auth.FacebookMixin):
- @tornado.web.asynchronous
+class AuthLogoutHandler(BaseHandler, tornado.auth.FacebookGraphMixin):
def get(self):
self.clear_cookie("user")
- if not self.current_user:
- self.redirect(self.get_argument("next", "/"))
- return
- self.facebook_request(
- method="auth.revokeAuthorization",
- callback=self.async_callback(self._on_deauthorize),
- session_key=self.current_user["session_key"])
-
- def _on_deauthorize(self, response):
self.redirect(self.get_argument("next", "/"))
class PostModule(tornado.web.UIModule):
- def render(self, post, actor):
- return self.render_string("modules/post.html", post=post, actor=actor)
+ def render(self, post):
+ return self.render_string("modules/post.html", post=post)
def main():
View
26 demos/facebook/templates/modules/post.html
@@ -1,29 +1,17 @@
<div class="post">
<div class="picture">
- <a href="{{ actor["url"] }}"><img src="{{ actor["pic_square"] }}"/></a>
+ {% set author_url="http://www.facebook.com/profile.php?id=" + escape(post["from"]["id"]) %}
+ <a href="{{ author_url }}"><img src="//graph.facebook.com/{{ escape(post["from"]["id"]) }}/picture?type=square"/></a>
</div>
<div class="body">
- <a href="{{ actor["url"] }}" class="actor">{{ escape(actor["name"]) }}</a>
- {% if post["message"] %}
+ <a href="{{ author_url }}" class="actor">{{ escape(post["from"]["name"]) }}</a>
+ {% if "message" in post %}
<span class="message">{{ escape(post["message"]) }}</span>
{% end %}
- {% if post["attachment"] %}
- <div class="attachment">
- {% if post["attachment"].get("name") %}
- <div class="name"><a href="{{ post["attachment"]["href"] }}">{{ escape(post["attachment"]["name"]) }}</a></div>
- {% end %}
- {% if post["attachment"].get("description") %}
- <div class="description">{{ post["attachment"]["description"] }}</div>
- {% end %}
- {% for media in filter(lambda m: m.get("src") and m["type"] in ("photo", "link"), post["attachment"].get("media", [])) %}
- <span class="media">
- <a href="{{ media["href"] }}"><img src="{{ media["src"] }}" alt="{{ escape(media.get("alt", "")) }}"/></a>
- </span>
- {% end %}
- </div>
- {% end %}
<div class="meta">
- <a href="{{ post["permalink"] }}" class="permalink">{{ locale.format_date(post["created_time"]) }}</a>
+ {% if "actions" in post %}
+ <a href="{{ escape(post["actions"][0]["link"]) }}" class="permalink">{{ locale.format_date(datetime.datetime.strptime(post["created_time"], "%Y-%m-%dT%H:%M:%S+0000")) }}</a>
+ {% end %}
</div>
</div>
</div>
View
4 demos/facebook/templates/stream.html
@@ -13,8 +13,8 @@
</div>
<div style="margin-bottom:1em"><a href="/">{{ _("Refresh stream") }}</a></div>
<div id="stream">
- {% for post in stream["posts"] %}
- {{ modules.Post(post, stream["profiles"][post["actor_id"]]) }}
+ {% for post in stream["data"] %}
+ {{ modules.Post(post) }}
{% end %}
</div>
</div>
View
22 demos/facebook/uimodules.py
@@ -1,22 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright 2009 Facebook
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-import tornado.web
-
-
-class Entry(tornado.web.UIModule):
- def render(self):
- return '<div>ENTRY</div>'
View
3 tornado/s3server.py → demos/s3server/s3server.py
@@ -42,6 +42,7 @@
from tornado import httpserver
from tornado import ioloop
from tornado import web
+from tornado.util import bytes_type
def start(port, root_directory="/tmp/s3", bucket_depth=0):
"""Starts the mock S3 server on the given port at the given path."""
@@ -86,7 +87,7 @@ def render_xml(self, value):
''.join(parts))
def _render_parts(self, value, parts=[]):
- if isinstance(value, basestring):
+ if isinstance(value, (unicode, bytes_type)):
parts.append(escape.xhtml_escape(value))
elif isinstance(value, int) or isinstance(value, long):
parts.append(str(value))
View
106 demos/websocket/chatdemo.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+#
+# Copyright 2009 Facebook
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""Simplified chat demo for websockets.
+
+Authentication, error handling, etc are left as an exercise for the reader :)
+"""
+
+import logging
+import tornado.escape
+import tornado.ioloop
+import tornado.options
+import tornado.web
+import tornado.websocket
+import os.path
+import uuid
+
+from tornado.options import define, options
+
+define("port", default=8888, help="run on the given port", type=int)
+
+
+class Application(tornado.web.Application):
+ def __init__(self):
+ handlers = [
+ (r"/", MainHandler),
+ (r"/chatsocket", ChatSocketHandler),
+ ]
+ settings = dict(
+ cookie_secret="43oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
+ template_path=os.path.join(os.path.dirname(__file__), "templates"),
+ static_path=os.path.join(os.path.dirname(__file__), "static"),
+ xsrf_cookies=True,
+ autoescape=None,
+ )
+ tornado.web.Application.__init__(self, handlers, **settings)
+
+
+class MainHandler(tornado.web.RequestHandler):
+ def get(self):
+ self.render("index.html", messages=ChatSocketHandler.cache)
+
+class ChatSocketHandler(tornado.websocket.WebSocketHandler):
+ waiters = set()
+ cache = []
+ cache_size = 200
+
+ def allow_draft76(self):
+ # for iOS 5.0 Safari
+ return True
+
+ def open(self):
+ ChatSocketHandler.waiters.add(self)
+
+ def on_close(self):
+ ChatSocketHandler.waiters.remove(self)
+
+ @classmethod
+ def update_cache(cls, chat):
+ cls.cache.append(chat)
+ if len(cls.cache) > cls.cache_size:
+ cls.cache = cls.cache[-cls.cache_size:]
+
+ @classmethod
+ def send_updates(cls, chat):
+ logging.info("sending message to %d waiters", len(cls.waiters))
+ for waiter in cls.waiters:
+ try:
+ waiter.write_message(chat)
+ except:
+ logging.error("Error sending message", exc_info=True)
+
+ def on_message(self, message):
+ logging.info("got message %r", message)
+ parsed = tornado.escape.json_decode(message)
+ chat = {
+ "id": str(uuid.uuid4()),
+ "body": parsed["body"],
+ }
+ chat["html"] = self.render_string("message.html", message=chat)
+
+ ChatSocketHandler.update_cache(chat)
+ ChatSocketHandler.send_updates(chat)
+
+
+def main():
+ tornado.options.parse_command_line()
+ app = Application()
+ app.listen(options.port)
+ tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == "__main__":
+ main()
View
56 demos/websocket/static/chat.css
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2009 FriendFeed
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License. You may obtain
+ * a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+body {
+ background: white;
+ margin: 10px;
+}
+
+body,
+input {
+ font-family: sans-serif;
+ font-size: 10pt;
+ color: black;
+}
+
+table {
+ border-collapse: collapse;
+ border: 0;
+}
+
+td {
+ border: 0;
+ padding: 0;
+}
+
+#body {
+ position: absolute;
+ bottom: 10px;
+ left: 10px;
+}
+
+#input {
+ margin-top: 0.5em;
+}
+
+#inbox .message {
+ padding-top: 0.25em;
+}
+
+#nav {
+ float: right;
+ z-index: 99;
+}
View
72 demos/websocket/static/chat.js
@@ -0,0 +1,72 @@
+// Copyright 2009 FriendFeed
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
+
+$(document).ready(function() {
+ if (!window.console) window.console = {};
+ if (!window.console.log) window.console.log = function() {};
+
+ $("#messageform").live("submit", function() {
+ newMessage($(this));
+ return false;
+ });
+ $("#messageform").live("keypress", function(e) {
+ if (e.keyCode == 13) {
+ newMessage($(this));
+ return false;
+ }
+ });
+ $("#message").select();
+ updater.start();
+});
+
+function newMessage(form) {
+ var message = form.formToDict();
+ updater.socket.send(JSON.stringify(message));
+ form.find("input[type=text]").val("").select();
+}
+
+jQuery.fn.formToDict = function() {
+ var fields = this.serializeArray();
+ var json = {}
+ for (var i = 0; i < fields.length; i++) {
+ json[fields[i].name] = fields[i].value;
+ }
+ if (json.next) delete json.next;
+ return json;
+};
+
+var updater = {
+ socket: null,
+
+ start: function() {
+ var url = "ws://" + location.host + "/chatsocket";
+ if ("WebSocket" in window) {
+ updater.socket = new WebSocket(url);
+ } else {
+ updater.socket = new MozWebSocket(url);
+ }
+ updater.socket.onmessage = function(event) {
+ updater.showMessage(JSON.parse(event.data));
+ }
+ },
+
+ showMessage: function(message) {
+ var existing = $("#m" + message.id);
+ if (existing.length > 0) return;
+ var node = $(message.html);
+ node.hide();
+ $("#inbox").append(node);
+ node.slideDown();
+ }
+};
View
33 demos/websocket/templates/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
+ <title>Tornado Chat Demo</title>
+ <link rel="stylesheet" href="{{ static_url("chat.css") }}" type="text/css"/>
+ </head>
+ <body>
+ <div id="body">
+ <div id="inbox">
+ {% for message in messages %}
+ {% include "message.html" %}
+ {% end %}
+ </div>
+ <div id="input">
+ <form action="/a/message/new" method="post" id="messageform">
+ <table>
+ <tr>
+ <td><input name="body" id="message" style="width:500px"/></td>
+ <td style="padding-left:5px">
+ <input type="submit" value="{{ _("Post") }}"/>
+ <input type="hidden" name="next" value="{{ request.path }}"/>
+ {{ xsrf_form_html() }}
+ </td>
+ </tr>
+ </table>
+ </form>
+ </div>
+ </div>
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3/jquery.min.js" type="text/javascript"></script>
+ <script src="{{ static_url("chat.js") }}" type="text/javascript"></script>
+ </body>
+</html>
View
2 demos/websocket/templates/message.html
@@ -0,0 +1,2 @@
+{% import tornado.escape %}
+<div class="message" id="m{{ message["id"] }}">{{ tornado.escape.linkify(message["body"]) }}</div>
View
3 maint/README
@@ -0,0 +1,3 @@
+This directory contains tools and scripts that are used in the development
+and maintainance of Tornado itself, but are probably not of interest to
+Tornado users.
View
8 maint/appengine/README
@@ -0,0 +1,8 @@
+Unit test support for app engine. Currently very limited as most of
+our tests depend on direct network access, but these tests ensure that the
+modules that are supposed to work on app engine don't depend on any
+forbidden modules.
+
+The code lives in maint/appengine/common, but should be run from the py25
+or py27 subdirectories (which contain an app.yaml and a bunch of symlinks).
+runtests.py is the entry point; cgi_runtests.py is used internally.
View
75 maint/appengine/common/cgi_runtests.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+import sys
+import unittest
+
+# Most of our tests depend on IOLoop, which is not importable on app engine.
+# Run the tests that work, and check that forbidden imports don't sneak
+# in to modules that are supposed to work on app engine.
+TEST_MODULES = [
+ 'tornado.httputil.doctests',
+ #'tornado.iostream.doctests',
+ 'tornado.util.doctests',
+ #'tornado.test.auth_test',
+ #'tornado.test.curl_httpclient_test',
+ 'tornado.test.escape_test',
+ #'tornado.test.gen_test',
+ #'tornado.test.httpclient_test',
+ #'tornado.test.httpserver_test',
+ 'tornado.test.httputil_test',
+ #'tornado.test.import_test',
+ #'tornado.test.ioloop_test',
+ #'tornado.test.iostream_test',
+ #'tornado.test.process_test',
+ #'tornado.test.simple_httpclient_test',
+ #'tornado.test.stack_context_test',
+ 'tornado.test.template_test',
+ #'tornado.test.testing_test',
+ #'tornado.test.twisted_test',
+ #'tornado.test.web_test',
+ #'tornado.test.wsgi_test',
+]
+
+def import_everything():
+ # import tornado.auth
+ # import tornado.autoreload
+ # import tornado.curl_httpclient # depends on pycurl
+ # import tornado.database # depends on MySQLdb
+ import tornado.escape
+ # import tornado.httpclient
+ # import tornado.httpserver
+ import tornado.httputil
+ # import tornado.ioloop
+ # import tornado.iostream
+ import tornado.locale
+ import tornado.options
+ # import tornado.netutil
+ # import tornado.platform.twisted # depends on twisted
+ # import tornado.process
+ # import tornado.simple_httpclient
+ import tornado.stack_context
+ import tornado.template
+ import tornado.testing
+ import tornado.util
+ import tornado.web
+ # import tornado.websocket
+ import tornado.wsgi
+
+def all():
+ return unittest.defaultTestLoader.loadTestsFromNames(TEST_MODULES)
+
+def main():
+ print "Content-Type: text/plain\r\n\r\n",
+
+ import_everything()
+
+ try:
+ unittest.main(defaultTest="all", argv=sys.argv)
+ except SystemExit, e:
+ if e.code == 0:
+ print "PASS"
+ else:
+ raise
+
+if __name__ == '__main__':
+ main()
+
View
53 maint/appengine/common/runtests.py
@@ -0,0 +1,53 @@
+#!/usr/bin/env python
+from __future__ import with_statement
+
+import contextlib
+import errno
+import os
+import random
+import signal
+import socket
+import subprocess
+import sys
+import time
+import urllib2
+
+if __name__ == "__main__":
+ tornado_root = os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '../../..'))
+ # dev_appserver doesn't seem to set SO_REUSEADDR
+ port = random.randrange(10000, 11000)
+ # does dev_appserver.py ever live anywhere but /usr/local/bin?
+ proc = subprocess.Popen([sys.executable,
+ "/usr/local/bin/dev_appserver.py",
+ os.path.dirname(os.path.abspath(__file__)),
+ "--port=%d" % port,
+ "--skip_sdk_update_check",
+ ],
+ cwd=tornado_root)
+
+ try:
+ for i in xrange(50):
+ with contextlib.closing(socket.socket()) as sock:
+ err = sock.connect_ex(('localhost', port))
+ if err == 0:
+ break
+ elif err != errno.ECONNREFUSED:
+ raise Exception("Got unexpected socket error %d" % err)
+ time.sleep(0.1)
+ else:
+ raise Exception("Server didn't start listening")
+
+ resp = urllib2.urlopen("http://localhost:%d/" % port)
+ print resp.read()
+ finally:
+ # dev_appserver sometimes ignores SIGTERM (especially on 2.5),
+ # so try a few times to kill it.
+ for sig in [signal.SIGTERM, signal.SIGTERM, signal.SIGKILL]:
+ os.kill(proc.pid, sig)
+ res = os.waitpid(proc.pid, os.WNOHANG)
+ if res != (0,0):
+ break
+ time.sleep(0.1)
+ else:
+ os.waitpid(proc.pid, 0)
View
8 maint/appengine/py25/app.yaml
@@ -0,0 +1,8 @@
+application: tornado-tests-appengine25
+version: 1
+runtime: python
+api_version: 1
+
+handlers:
+- url: /
+ script: cgi_runtests.py
View
1 maint/appengine/py25/cgi_runtests.py
View
1 maint/appengine/py25/runtests.py
View
1 maint/appengine/py25/tornado
View
9 maint/appengine/py27/app.yaml
@@ -0,0 +1,9 @@
+application: tornado-tests-appengine27
+version: 1
+runtime: python27
+threadsafe: false
+api_version: 1
+
+handlers:
+- url: /
+ script: cgi_runtests.py
View
1 maint/appengine/py27/cgi_runtests.py
View
1 maint/appengine/py27/runtests.py
View
1 maint/appengine/py27/tornado
View
4 maint/appengine/setup.py
@@ -0,0 +1,4 @@
+# Dummy setup file to make tox happy. In the appengine world things aren't
+# installed through setup.py
+import distutils.core
+distutils.core.setup()
View
19 maint/appengine/tox.ini
@@ -0,0 +1,19 @@
+# App Engine tests require the SDK to be installed separately.
+# Version 1.6.1 or newer is required (older versions don't work when
+# python is run from a virtualenv)
+#
+# These are currently excluded from the main tox.ini because their
+# logs are spammy and they're a little flaky.
+[tox]
+envlist = py25-appengine, py27-appengine
+
+[testenv]
+changedir = {toxworkdir}
+
+[testenv:py25-appengine]
+basepython = python2.5
+commands = python {toxinidir}/py25/runtests.py {posargs:}
+
+[testenv:py27-appengine]
+basepython = python2.7
+commands = python {toxinidir}/py27/runtests.py {posargs:}
View
23 maint/requirements.txt
@@ -0,0 +1,23 @@
+# Frozen pip requirements for tools used in the development of tornado
+
+# Tornado's optional dependencies
+MySQL-python==1.2.3
+Twisted==12.1.0
+pycurl==7.19.0
+
+# Other useful tools
+Sphinx==1.1.3
+autopep8==0.6.5
+coverage==3.5.2
+pep8==1.2
+pyflakes==0.5.0
+tox==1.4
+virtualenv==1.7.1.2
+
+# Indirect dependencies
+Jinja2==2.6
+Pygments==1.5
+docutils==0.9
+py==1.4.9
+wsgiref==0.1.2
+zope.interface==4.0.1
View
0 website/markdown/extensions/__init__.py → maint/scripts/custom_fixers/__init__.py
File renamed without changes.
View
60 maint/scripts/custom_fixers/fix_future_imports.py
@@ -0,0 +1,60 @@
+"""Updates all source files to import the same set of __future__ directives.
+"""
+from lib2to3 import fixer_base
+from lib2to3 import pytree
+from lib2to3.pgen2 import token
+from lib2to3.fixer_util import FromImport, Name, Comma, Newline
+
+# copied from fix_tuple_params.py
+def is_docstring(stmt):
+ return isinstance(stmt, pytree.Node) and \
+ stmt.children[0].type == token.STRING
+
+class FixFutureImports(fixer_base.BaseFix):
+ BM_compatible = True
+
+ PATTERN = """import_from< 'from' module_name="__future__" 'import' any >"""
+
+ def start_tree(self, tree, filename):
+ self.found_future_import = False
+
+ def new_future_import(self, old):
+ new = FromImport("__future__",
+ [Name("absolute_import", prefix=" "), Comma(),
+ Name("division", prefix=" "), Comma(),
+ Name("with_statement", prefix=" ")])
+ if old is not None:
+ new.prefix = old.prefix
+ return new
+
+ def transform(self, node, results):
+ self.found_future_import = True
+ return self.new_future_import(node)
+
+ def finish_tree(self, tree, filename):
+ if self.found_future_import:
+ return
+ if not isinstance(tree, pytree.Node):
+ # Empty files (usually __init__.py) show up as a single Leaf
+ # instead of a Node, so leave them alone
+ return
+ first_stmt = tree.children[0]
+ if is_docstring(first_stmt):
+ # Skip a line and add the import after the docstring
+ tree.insert_child(1, Newline())
+ pos = 2
+ elif first_stmt.prefix:
+ # No docstring, but an initial comment (perhaps a #! line).
+ # Transfer the initial comment to a new blank line.
+ newline = Newline()
+ newline.prefix = first_stmt.prefix
+ first_stmt.prefix = ""
+ tree.insert_child(0, newline)
+ pos = 1
+ else:
+ # No comments or docstring, just insert at the start
+ pos = 0
+ tree.insert_child(pos, self.new_future_import(None))
+ tree.insert_child(pos+1, Newline()) # terminates the import stmt
+
+
View
8 maint/scripts/run_autopep8.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+# Runs autopep8 in the configuration used for tornado.
+#
+# W602 is "deprecated form of raising exception", but the fix is incorrect
+# (and I'm not sure if the three-argument form of raise is really deprecated
+# in the first place)
+autopep8 --ignore=W602 -i tornado/*.py tornado/platform/*.py tornado/test/*.py
View
6 maint/scripts/run_fixers.py
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+
+import sys
+from lib2to3.main import main
+
+sys.exit(main("custom_fixers"))
View
3 maint/test/README
@@ -0,0 +1,3 @@
+This directory contains additional tests that are not included in the main
+suite (because e.g. they have extra dependencies, run slowly, or produce
+more output than a simple pass/fail)
View
1 maint/test/websocket/.gitignore
@@ -0,0 +1 @@
+reports/
View
32 maint/test/websocket/client.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+import sys
+from tornado.options import options, define, parse_command_line
+from twisted.python import log
+from twisted.internet import reactor
+from autobahn.fuzzing import FuzzingClientFactory
+
+define('servers', type=str, multiple=True,
+ default=['Tornado=ws://localhost:9000'])
+
+define('cases', type=str, multiple=True,
+ default=["*"])
+define('exclude', type=str, multiple=True,
+ default=["9.*"])
+
+if __name__ == '__main__':
+ parse_command_line()
+ log.startLogging(sys.stdout)
+ servers = []
+ for server in options.servers:
+ name, _, url = server.partition('=')
+ servers.append({"agent": name, "url": url, "options": {"version": 17}})
+ spec = {
+ "options": {"failByDrop": False},
+ "enable-ssl": False,
+ "servers": servers,
+ "cases": options.cases,
+ "exclude-cases": options.exclude,
+ "exclude-agent-cases": {},
+ }
+ fuzzer = FuzzingClientFactory(spec)
+ reactor.run()
View
37 maint/test/websocket/run.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+#
+# Runs the autobahn websocket conformance test against tornado in both
+# python2 and python3. Output goes in ./reports/servers/index.html.
+#
+# The --cases and --exclude arguments can be used to run only part of
+# the suite. The default is --exclude="9.*" to skip the relatively slow
+# performance tests; pass --exclude="" to override and include them.
+
+set -e
+
+# build/update the virtualenvs
+tox
+
+.tox/py25/bin/python server.py --port=9001 &
+PY25_SERVER_PID=$!
+
+.tox/py27/bin/python server.py --port=9002 &
+PY27_SERVER_PID=$!
+
+.tox/py32/bin/python server.py --port=9003 &
+PY32_SERVER_PID=$!
+
+.tox/pypy/bin/python server.py --port=9004 &
+PYPY_SERVER_PID=$!
+
+sleep 1
+
+.tox/py27/bin/python ./client.py --servers=Tornado/py25=ws://localhost:9001,Tornado/py27=ws://localhost:9002,Tornado/py32=ws://localhost:9003,Tornado/pypy=ws://localhost:9004 "$@" || true
+
+kill $PY25_SERVER_PID
+kill $PY27_SERVER_PID
+kill $PY32_SERVER_PID
+kill $PYPY_SERVER_PID
+wait
+
+echo "Tests complete. Output is in ./reports/servers/index.html"
View
21 maint/test/websocket/server.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+from tornado.ioloop import IOLoop
+from tornado.options import define, options, parse_command_line
+from tornado.util import bytes_type
+from tornado.websocket import WebSocketHandler
+from tornado.web import Application
+
+define('port', default=9000)
+
+class EchoHandler(WebSocketHandler):
+ def on_message(self, message):
+ self.write_message(message, binary=isinstance(message, bytes_type))
+
+if __name__ == '__main__':
+ parse_command_line()
+ app = Application([
+ ('/', EchoHandler),
+ ])
+ app.listen(options.port, address='127.0.0.1')
+ IOLoop.instance().start()
View
12 maint/test/websocket/tox.ini
@@ -0,0 +1,12 @@
+# We don't actually use tox to run this test, but it's the easiest way
+# to install autobahn and deal with 2to3 for the python3 version.
+# See run.sh for the real test runner.
+[tox]
+envlist = py27, py32, py25, pypy
+setupdir=../../..
+
+[testenv]
+commands = python -c pass
+
+[testenv:py27]
+deps = autobahn
View
23 maint/vm/README
@@ -0,0 +1,23 @@
+This directory contains virtual machine setup scripts for testing Tornado.
+
+Requirements:
+
+Vagrant (http://vagrantup.com) and VirtualBox (http://virtualbox.org).
+Vagrant provides an easy download for Ubuntu 10.04 (aka lucid64); base
+images for other platforms are harder to find and can be built with
+VeeWee (https://github.com/jedi4ever/veewee).
+
+Usage:
+
+cd to the appropriate directory and run `vagrant up`, then `vagrant ssh`.
+From there, simply run `tox` to run the full test suite, or cd to /tornado
+and test manually. Afterwards, use `vagrant suspend` or `vagrant destroy`
+to clean up.
+
+Notes:
+
+Python distutils (and therefore tox) assume that if the platform
+supports hard links, they can be used in the Tornado source directory.
+VirtualBox's shared folder filesystem does not support hard links (or
+symlinks), so we have to use NFS shared folders instead. (which has
+the unfortunate side effect of requiring sudo on the host machine)
View
23 maint/vm/freebsd/Vagrantfile
@@ -0,0 +1,23 @@
+Vagrant::Config.run do |config|
+ # A freebsd image can be created with veewee
+ # https://github.com/jedi4ever/veewee
+ #
+ # vagrant basebox define freebsd freebsd-8.2-pcbsd-i386-netboot
+ # vagrant basebox build freebsd
+ # vagrant basebox export freebsd
+ # vagrant box add freebsd freebsd.box
+ config.vm.box = "freebsd"
+
+ config.vm.guest = :freebsd
+
+ # Note that virtualbox shared folders don't work with freebsd, so
+ # we'd need nfs shared folders here even if virtualbox gains
+ # support for symlinks.
+ config.vm.network :hostonly, "172.19.1.3"
+ # Name this v-root to clobber the default /vagrant mount point.
+ # We can't mount it over nfs because there are apparently issues
+ # when one nfs export is a subfolder of another.
+ config.vm.share_folder("v-root", "/tornado", "../../..", :nfs => true)
+
+ config.vm.provision :shell, :path => "setup.sh"
+end
View
30 maint/vm/freebsd/setup.sh
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+chsh -s bash vagrant
+
+# This doesn't get created automatically for freebsd since virtualbox
+# shared folders don't work.
+ln -snf /tornado/maint/vm/freebsd /vagrant
+
+PORTS="
+lang/python27
+devel/py-pip
+devel/py-virtualenv
+ftp/curl
+"
+
+PIP_PACKAGES="
+tox
+pycurl
+"
+
+cd /usr/ports
+
+for port in $PORTS; do
+ make -C $port -DBATCH install
+done
+
+pip install $PIP_PACKAGES
+
+/tornado/maint/vm/shared-setup.sh
+
View
13 maint/vm/freebsd/tox.ini
@@ -0,0 +1,13 @@
+[tox]
+envlist=py27-full, py27
+setupdir=/tornado
+# /home is a symlink to /usr/home, but tox doesn't like symlinks here
+toxworkdir=/usr/home/vagrant/tox-tornado
+
+[testenv]
+commands = python -m tornado.test.runtests {posargs:}
+
+[testenv:py27-full]
+# other dependencies aren't really worth the install time
+deps =
+ pycurl
View
9 maint/vm/shared-setup.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+# Run at the end of each vm's provisioning script
+
+set -e
+
+# Link tox.ini into the home directory so you can run tox immediately
+# after ssh'ing in without cd'ing to /vagrant (since cd'ing to /tornado
+# gets the wrong config)
+ln -sf /vagrant/tox.ini ~vagrant/tox.ini
View
9 maint/vm/ubuntu10.04/Vagrantfile
@@ -0,0 +1,9 @@
+Vagrant::Config.run do |config|
+ config.vm.box = "lucid64"
+ config.vm.box_url = "http://files.vagrantup.com/lucid64.box"
+
+ config.vm.network :hostonly, "172.19.1.2"
+ config.vm.share_folder("tornado", "/tornado", "../../..", :nfs=> true)
+
+ config.vm.provision :shell, :path => "setup.sh"
+end
View
50 maint/vm/ubuntu10.04/setup.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+set -e
+
+apt-get update
+
+# libcurl4-gnutls-dev is the default if you ask for libcurl4-dev, but it
+# has bugs that make our tests deadlock (the relevant tests detect this and
+# disable themselves, but it means that to get full coverage we have to use
+# the openssl version).
+# The oddly-named python-software-properties includes add-apt-repository.
+APT_PACKAGES="
+python-pip
+python-dev
+libmysqlclient-dev
+libcurl4-openssl-dev
+python-software-properties
+"
+
+apt-get -y install $APT_PACKAGES
+
+
+# Ubuntu 10.04 has python 2.6 as default; install more from here.
+# The most important thing is to have both 2.5 and a later version so we
+# test with both tornado.epoll and 2.6+ stdlib's select.epoll.
+add-apt-repository ppa:fkrull/deadsnakes
+apt-get update
+
+DEADSNAKES_PACKAGES="
+python2.5
+python2.5-dev
+python2.7
+python2.7-dev
+python3.2
+python3.2-dev
+"
+apt-get -y install $DEADSNAKES_PACKAGES
+
+
+PIP_PACKAGES="
+virtualenv
+tox
+MySQL-python
+pycurl
+twisted
+"
+
+pip install $PIP_PACKAGES
+
+/tornado/maint/vm/shared-setup.sh
View
33 maint/vm/ubuntu10.04/tox.ini
@@ -0,0 +1,33 @@
+[tox]
+envlist = py27-full, py25-full, py32, py25, py26, py26-full, py27
+setupdir=/tornado
+toxworkdir=/home/vagrant/tox-tornado
+
+[testenv]
+commands = python -m tornado.test.runtests {posargs:}
+
+[testenv:py25]
+basepython = python2.5
+deps = simplejson
+
+[testenv:py25-full]
+basepython = python2.5
+deps =
+ MySQL-python
+ pycurl
+ simplejson
+ twisted==11.0.0
+ zope.interface<4.0
+
+[testenv:py26-full]
+deps =
+ MySQL-python
+ pycurl
+ twisted==11.0.0
+
+[testenv:py27-full]
+basepython = python2.7
+deps =
+ MySQL-python
+ pycurl
+ twisted==11.0.0
View
8 maint/vm/ubuntu12.04/Vagrantfile
@@ -0,0 +1,8 @@
+Vagrant::Config.run do |config|
+ config.vm.box = "ubuntu12.04"
+
+ config.vm.network :hostonly, "172.19.1.5"
+ config.vm.share_folder("tornado", "/tornado", "../../..", :nfs=> true)
+
+ config.vm.provision :shell, :path => "setup.sh"
+end
View
48 maint/vm/ubuntu12.04/setup.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+
+set -e
+
+apt-get update
+
+# libcurl4-gnutls-dev is the default if you ask for libcurl4-dev, but it
+# has bugs that make our tests deadlock (the relevant tests detect this and
+# disable themselves, but it means that to get full coverage we have to use
+# the openssl version).
+# The oddly-named python-software-properties includes add-apt-repository.
+APT_PACKAGES="
+python-pip
+python-dev
+libmysqlclient-dev
+libcurl4-openssl-dev
+python-software-properties
+"
+
+apt-get -y install $APT_PACKAGES
+
+
+# Ubuntu 12.04 has python 2.7 as default; install more from here.
+# The most important thing is to have both 2.5 and a later version so we
+# test with both tornado.epoll and 2.6+ stdlib's select.epoll.
+add-apt-repository ppa:fkrull/deadsnakes
+apt-get update
+
+DEADSNAKES_PACKAGES="
+python2.5
+python2.5-dev
+python3.2
+python3.2-dev
+"
+apt-get -y install $DEADSNAKES_PACKAGES
+
+
+PIP_PACKAGES="
+virtualenv
+tox
+MySQL-python
+pycurl
+twisted
+"
+
+pip install $PIP_PACKAGES
+
+/tornado/maint/vm/shared-setup.sh
View
27 maint/vm/ubuntu12.04/tox.ini
@@ -0,0 +1,27 @@
+[tox]
+envlist = py27-full, py25-full, py32, py25, py27
+setupdir=/tornado
+toxworkdir=/home/vagrant/tox-tornado
+
+[testenv]
+commands = python -m tornado.test.runtests {posargs:}
+
+[testenv:py25]
+basepython = python2.5
+deps = simplejson
+
+[testenv:py25-full]
+basepython = python2.5
+deps =
+ MySQL-python
+ pycurl
+ simplejson
+ twisted==11.0.0
+ zope.interface<4.0
+
+[testenv:py27-full]
+basepython = python2.7
+deps =
+ MySQL-python
+ pycurl
+ twisted==11.0.0
View
10 runtests.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+cd $(dirname $0)
+
+# "python -m" differs from "python tornado/test/runtests.py" in how it sets
+# up the default python path. "python -m" uses the current directory,
+# while "python file.py" uses the directory containing "file.py" (which is
+# not what you want if file.py appears within a package you want to import
+# from)
+python -m tornado.test.runtests "$@"
View
30 setup.py
@@ -23,6 +23,8 @@
except ImportError:
pass
+kwargs = {}
+
# Build the epoll extension for Linux systems with Python < 2.6
extensions = []
major, minor = sys.version_info[:2]
@@ -31,14 +33,38 @@
extensions.append(distutils.core.Extension(
"tornado.epoll", ["tornado/epoll.c"]))
+version = "2.3.post1"
+
+if major >= 3:
+ import setuptools # setuptools is required for use_2to3
+ kwargs["use_2to3"] = True
+
distutils.core.setup(
name="tornado",
- version="1.0",
- packages = ["tornado"],
+ version=version,
+ packages = ["tornado", "tornado.test", "tornado.platform"],
+ package_data = {
+ "tornado": ["ca-certificates.crt"],
+ # data files need to be listed both here (which determines what gets
+ # installed) and in MANIFEST.in (which determines what gets included
+ # in the sdist tarball)
+ "tornado.test": [
+ "README",
+ "test.crt",
+ "test.key",
+ "static/robots.txt",
+ "templates/utf8.html",
+ "csv_translations/fr_FR.csv",
+ "gettext_translations/fr_FR/LC_MESSAGES/tornado_test.mo",
+ "gettext_translations/fr_FR/LC_MESSAGES/tornado_test.po",
+ ],
+ },
ext_modules = extensions,
author="Facebook",
author_email="python-tornado@googlegroups.com",
url="http://www.tornadoweb.org/",
+ download_url="http://github.com/downloads/facebook/tornado/tornado-%s.tar.gz" % version,
license="http://www.apache.org/licenses/LICENSE-2.0",
description="Tornado is an open source version of the scalable, non-blocking web server and and tools that power FriendFeed",
+ **kwargs
)
View
12 tornado/__init__.py
@@ -15,3 +15,15 @@
# under the License.
"""The Tornado web server and tools."""
+
+from __future__ import absolute_import, division, with_statement
+
+# version is a human-readable version number.
+
+# version_info is a four-tuple for programmatic comparison. The first
+# three numbers are the components of the version number. The fourth
+# is zero for an official release, positive for a development branch,
+# or negative for a release candidate (after the base version number
+# has been incremented)
+version = "2.3.post1"
+version_info = (2, 3, 0, 1)
View
647 tornado/auth.py
@@ -28,25 +28,26 @@
services implement authentication and authorization slightly differently.
See the individual service classes below for complete documentation.
-Example usage for Google OpenID:
+Example usage for Google OpenID::
-class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
- @tornado.web.asynchronous
- def get(self):
- if self.get_argument("openid.mode", None):
- self.get_authenticated_user(self.async_callback(self._on_auth))
- return
- self.authenticate_redirect()
-
- def _on_auth(self, user):
- if not user:
- raise tornado.web.HTTPError(500, "Google auth failed")
- # Save the user with, e.g., set_secure_cookie()
+ class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
+ @tornado.web.asynchronous
+ def get(self):
+ if self.get_argument("openid.mode", None):
+ self.get_authenticated_user(self.async_callback(self._on_auth))
+ return
+ self.authenticate_redirect()
+ def _on_auth(self, user):
+ if not user:
+ raise tornado.web.HTTPError(500, "Google auth failed")
+ # Save the user with, e.g., set_secure_cookie()
"""
+from __future__ import absolute_import, division, with_statement
+
+import base64
import binascii
-import cgi
import hashlib
import hmac
import logging
@@ -57,14 +58,17 @@ def _on_auth(self, user):
from tornado import httpclient
from tornado import escape
+from tornado.httputil import url_concat
+from tornado.util import bytes_type, b
+
class OpenIdMixin(object):
"""Abstract implementation of OpenID and Attribute Exchange.
See GoogleMixin below for example implementations.
"""
def authenticate_redirect(self, callback_uri=None,
- ax_attrs=["name","email","language","username"]):
+ ax_attrs=["name", "email", "language", "username"]):
"""Returns the authentication URL for this service.
After authentication, the service will redirect back to the given
@@ -75,11 +79,11 @@ def authenticate_redirect(self, callback_uri=None,
all those attributes for your app, you can request fewer with
the ax_attrs keyword argument.
"""
- callback_uri = callback_uri or self.request.path
+ callback_uri = callback_uri or self.request.uri
args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
- def get_authenticated_user(self, callback):
+ def get_authenticated_user(self, callback, http_client=None):
"""Fetches the authenticated user data upon redirect.
This method should be called by the handler that receives the
@@ -90,8 +94,9 @@ def get_authenticated_user(self, callback):
args = dict((k, v[-1]) for k, v in self.request.arguments.iteritems())
args["openid.mode"] = u"check_authentication"
url = self._OPENID_ENDPOINT
- http = httpclient.AsyncHTTPClient()
- http.fetch(url, self.async_callback(
+ if http_client is None:
+ http_client = httpclient.AsyncHTTPClient()
+ http_client.fetch(url, self.async_callback(
self._on_authentication_verified, callback),
method="POST", body=urllib.urlencode(args))
@@ -104,7 +109,7 @@ def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
"openid.identity":
"http://specs.openid.net/auth/2.0/identifier_select",
"openid.return_to": url,
- "openid.realm": self.request.protocol + "://" + self.request.host + "/",
+ "openid.realm": urlparse.urljoin(url, '/'),
"openid.mode": "checkid_setup",
}
if ax_attrs:
@@ -144,29 +149,32 @@ def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
return args
def _on_authentication_verified(self, callback, response):
- if response.error or u"is_valid:true" not in response.body:
+ if response.error or b("is_valid:true") not in response.body:
logging.warning("Invalid OpenID response: %s", response.error or
response.body)
callback(None)
return
# Make sure we got back at least an email from attribute exchange