]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Major update to tornado.platform.twisted.
authorBen Darnell <ben@bendarnell.com>
Tue, 17 Jan 2012 07:23:21 +0000 (23:23 -0800)
committerBen Darnell <ben@bendarnell.com>
Tue, 17 Jan 2012 07:30:41 +0000 (23:30 -0800)
Significantly improved compatibility (most important changes are in
TornadoReactor._invoke_callback) and expanded test coverage.

tornado/platform/twisted.py
tornado/test/twisted_test.py
tox.ini
website/sphinx/releases/next.rst
website/sphinx/twisted.rst

index 25fe36f3513b8082d4f22c261368f7ccc9c2f8fb..5d406d3471c6b502bf000a2a1aaa990f821969b7 100644 (file)
 # License for the specific language governing permissions and limitations
 # under the License.
 
-"""
-A twisted-style reactor for the Tornado IOLoop.
+# Note:  This module's docs are not currently extracted automatically,
+# so changes must be made manually to twisted.rst
+# TODO: refactor doc build process to use an appropriate virtualenv
+"""A Twisted reactor built on the Tornado IOLoop.
+
+This module lets you run applications and libraries written for
+Twisted in a Tornado application.  To use it, simply call `install` at
+the beginning of the application::
+
+    import tornado.platform.twisted
+    tornado.platform.twisted.install()
+    from twisted.internet import reactor
+
+When the app is ready to start, call `IOLoop.instance().start()`
+instead of `reactor.run()`.  This will allow you to use a mixture of
+Twisted and Tornado code in the same process.
+
+It is also possible to create a non-global reactor by calling
+`tornado.platform.twisted.TornadoReactor(io_loop)`.  However, if
+the `IOLoop` and reactor are to be short-lived (such as those used in
+unit tests), additional cleanup may be required.  Specifically, it is
+recommended to call::
+
+    reactor.fireSystemEvent('shutdown')
+    reactor.disconnectAll()
 
-To use it, add the following to your twisted application:
+before closing the `IOLoop`.
 
-import tornado.platform.twisted
-tornado.platform.twisted.install()
-from twisted.internet import reactor
+This module has been tested with Twisted versions 11.0.0 and 11.1.0.
 """
 
 from __future__ import with_statement, absolute_import
@@ -32,7 +53,7 @@ import time
 from twisted.internet.posixbase import PosixReactorBase
 from twisted.internet.interfaces import \
     IReactorFDSet, IDelayedCall, IReactorTime
-from twisted.python import failure
+from twisted.python import failure, log
 from twisted.internet import error
 
 from zope.interface import implements
@@ -44,9 +65,7 @@ from tornado.ioloop import IOLoop
 
 
 class TornadoDelayedCall(object):
-    """
-    DelayedCall object for Tornado.
-    """
+    """DelayedCall object for Tornado."""
     implements(IDelayedCall)
 
     def __init__(self, reactor, seconds, f, *args, **kw):
@@ -89,8 +108,14 @@ class TornadoDelayedCall(object):
         return self._active
 
 class TornadoReactor(PosixReactorBase):
-    """
-    Twisted style reactor for Tornado.
+    """Twisted reactor built on the Tornado IOLoop.
+
+    Since it is intented to be used in applications where the top-level
+    event loop is ``io_loop.start()`` rather than ``reactor.run()``,
+    it is implemented a little differently than other Twisted reactors.
+    We override `mainLoop` instead of `doIteration` and must implement
+    timed call functionality on top of `IOLoop.add_timeout` rather than
+    using the implementation in `PosixReactorBase`.
     """
     implements(IReactorTime, IReactorFDSet)
 
@@ -98,14 +123,20 @@ class TornadoReactor(PosixReactorBase):
         if not io_loop:
             io_loop = tornado.ioloop.IOLoop.instance()
         self._io_loop = io_loop
-        self._readers = {}
-        self._writers = {}
+        self._readers = {}  # map of reader objects to fd
+        self._writers = {}  # map of writer objects to fd
         self._fds = {} # a map of fd to a (reader, writer) tuple
         self._delayedCalls = {}
-        self._running = False
-        self._closed = False
         PosixReactorBase.__init__(self)
 
+        # IOLoop.start() bypasses some of the reactor initialization.
+        # Fire off the necessary events if they weren't already triggered
+        # by reactor.run().
+        def start_if_necessary():
+            if not self._started:
+                self.fireSystemEvent('startup')
+        self._io_loop.add_callback(start_if_necessary)
+
     # IReactorTime
     def seconds(self):
         return time.time()
@@ -124,9 +155,7 @@ class TornadoReactor(PosixReactorBase):
 
     # IReactorThreads
     def callFromThread(self, f, *args, **kw):
-        """
-        See L{twisted.internet.interfaces.IReactorThreads.callFromThread}.
-        """
+        """See `twisted.internet.interfaces.IReactorThreads.callFromThread`"""
         assert callable(f), "%s is not callable" % f
         p = functools.partial(f, *args, **kw)
         self._io_loop.add_callback(p)
@@ -142,25 +171,36 @@ class TornadoReactor(PosixReactorBase):
     # IReactorFDSet
     def _invoke_callback(self, fd, events):
         (reader, writer) = self._fds[fd]
-        if events & IOLoop.READ and reader:
-            reader.doRead()
-        if events & IOLoop.WRITE and writer:
-            writer.doWrite()
-       if events & IOLoop.ERROR:
-           if reader:
-               reader.readConnectionLost(failure.Failure(error.ConnectionLost()))
-           if writer:
-               writer.connectionLost(failure.Failure(error.ConnectionLost()))
+        if reader:
+            err = None
+            if reader.fileno() == -1:
+                err = error.ConnectionLost()
+            elif events & IOLoop.READ:
+                err = log.callWithLogger(reader, reader.doRead)
+            if err is None and events & IOLoop.ERROR:
+                err = error.ConnectionLost()
+            if err is not None:
+                self.removeReader(reader)
+                reader.readConnectionLost(failure.Failure(err))
+        if writer:
+            err = None
+            if writer.fileno() == -1:
+                err = error.ConnectionLost()
+            elif events & IOLoop.WRITE:
+                err = log.callWithLogger(writer, writer.doWrite)
+            if err is None and events & IOLoop.ERROR:
+                err = error.ConnectionLost()
+            if err is not None:
+                self.removeWriter(writer)
+                writer.writeConnectionLost(failure.Failure(err))
 
     def addReader(self, reader):
-        """
-        Add a FileDescriptor for notification of data available to read.
-        """
+        """Add a FileDescriptor for notification of data available to read."""
         if reader in self._readers:
             # Don't add the reader if it's already there
             return
-        self._readers[reader] = True
         fd = reader.fileno()
+        self._readers[reader] = fd
         if fd in self._fds:
             (_, writer) = self._fds[fd]
             self._fds[fd] = (reader, writer)
@@ -175,13 +215,11 @@ class TornadoReactor(PosixReactorBase):
                                          IOLoop.READ)
 
     def addWriter(self, writer):
-        """
-        Add a FileDescriptor for notification of data available to write.
-        """
+        """Add a FileDescriptor for notification of data available to write."""
         if writer in self._writers:
             return
-        self._writers[writer] = True
         fd = writer.fileno()
+        self._writers[writer] = fd
         if fd in self._fds:
             (reader, _) = self._fds[fd]
             self._fds[fd] = (reader, writer)
@@ -196,13 +234,9 @@ class TornadoReactor(PosixReactorBase):
                                          IOLoop.WRITE)
 
     def removeReader(self, reader):
-        """
-        Remove a Selectable for notification of data available to read.
-        """
-        fd = reader.fileno()
+        """Remove a Selectable for notification of data available to read."""
         if reader in self._readers:
-            del self._readers[reader]
-            if self._closed: return
+            fd = self._readers.pop(reader)
             (_, writer) = self._fds[fd]
             if writer:
                 # We have a writer so we need to update the IOLoop for
@@ -217,13 +251,9 @@ class TornadoReactor(PosixReactorBase):
                 self._io_loop.remove_handler(fd)
 
     def removeWriter(self, writer):
-        """
-        Remove a Selectable for notification of data available to write.
-        """
-        fd = writer.fileno()
+        """Remove a Selectable for notification of data available to write."""
         if writer in self._writers:
-            del self._writers[writer]
-            if self._closed: return
+            fd = self._writers.pop(writer)
             (reader, _) = self._fds[fd]
             if reader:
                 # We have a reader so we need to update the IOLoop for
@@ -246,47 +276,30 @@ class TornadoReactor(PosixReactorBase):
     def getWriters(self):
         return self._writers.keys()
 
+    # The following functions are mainly used in twisted-style test cases;
+    # it is expected that most users of the TornadoReactor will call
+    # IOLoop.start() instead of Reactor.run().
     def stop(self):
-        """
-        Implement L{IReactorCore.stop}.
-        """
-        self._running = False
         PosixReactorBase.stop(self)
-        self.runUntilCurrent()
-        try:
-            self._io_loop.stop()
-            self._io_loop.close()
-        except:
-            # Ignore any exceptions thrown by IOLoop
-            pass
-        self._closed = True
+        self._io_loop.stop()
 
     def crash(self):
-        if not self._running:
-            return
-        self._running = False
         PosixReactorBase.crash(self)
-        self.runUntilCurrent()
-        try:
-            self._io_loop.stop()
-            self._io_loop.close()
-        except:
-            # Ignore any exceptions thrown by IOLoop
-            pass
-        self._closed = True
+        self._io_loop.stop()
 
     def doIteration(self, delay):
         raise NotImplementedError("doIteration")
 
     def mainLoop(self):
-        self._running = True
         self._io_loop.start()
+        if self._stopped:
+            self.fireSystemEvent("shutdown")
 
 class _TestReactor(TornadoReactor):
     """Subclass of TornadoReactor for use in unittests.
 
     This can't go in the test.py file because of import-order dependencies
-    with the twisted reactor test builder.
+    with the Twisted reactor test builder.
     """
     def __init__(self):
         # always use a new ioloop
@@ -299,12 +312,16 @@ class _TestReactor(TornadoReactor):
         return super(_TestReactor, self).listenTCP(
             port, factory, backlog=backlog, interface=interface)
 
+    def listenUDP(self, port, protocol, interface='', maxPacketSize=8192):
+        if not interface:
+            interface = '127.0.0.1'
+        return super(_TestReactor, self).listenUDP(
+            port, protocol, interface=interface, maxPacketSize=maxPacketSize)
+
 
 
 def install(io_loop=None):
-    """
-    Install the Tornado reactor.
-    """
+    """Install this package as the default Twisted reactor."""
     if not io_loop:
         io_loop = tornado.ioloop.IOLoop.instance()
     reactor = TornadoReactor(io_loop)
index 56312458fabc01b16ef32f9a3e1dc89d7ff10bd3..ba53c7897605c53e42ca09b14f4a3754a36b3e88 100644 (file)
@@ -25,7 +25,13 @@ import unittest
 try:
     import fcntl
     import twisted
+    from twisted.internet.defer import Deferred
     from twisted.internet.interfaces import IReadDescriptor, IWriteDescriptor
+    from twisted.internet.protocol import Protocol
+    from twisted.web.client import Agent
+    from twisted.web.resource import Resource
+    from twisted.web.server import Site
+    from twisted.python import log
     from tornado.platform.twisted import TornadoReactor
     from zope.interface import implements
 except ImportError:
@@ -34,14 +40,22 @@ except ImportError:
     IReadDescriptor = IWriteDescriptor = None
     def implements(f): pass
 
+from tornado.httpclient import AsyncHTTPClient
 from tornado.ioloop import IOLoop
 from tornado.platform.auto import set_close_exec
+from tornado.testing import get_unused_port
 from tornado.util import import_object
+from tornado.web import RequestHandler, Application
 
-class ReactorWhenRunningTest(unittest.TestCase):
+class ReactorTestCase(unittest.TestCase):
     def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
+        self._io_loop = IOLoop()
+        self._reactor = TornadoReactor(self._io_loop)
 
+    def tearDown(self):
+        self._io_loop.close(all_fds=True)
+
+class ReactorWhenRunningTest(ReactorTestCase):
     def test_whenRunning(self):
         self._whenRunningCalled = False
         self._anotherWhenRunningCalled = False
@@ -58,10 +72,7 @@ class ReactorWhenRunningTest(unittest.TestCase):
     def anotherWhenRunningCallback(self):
         self._anotherWhenRunningCalled = True
 
-class ReactorCallLaterTest(unittest.TestCase):
-    def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
-
+class ReactorCallLaterTest(ReactorTestCase):
     def test_callLater(self):
         self._laterCalled = False
         self._now = self._reactor.seconds()
@@ -78,10 +89,7 @@ class ReactorCallLaterTest(unittest.TestCase):
         self._called = self._reactor.seconds()
         self._reactor.stop()
 
-class ReactorTwoCallLaterTest(unittest.TestCase):
-    def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
-
+class ReactorTwoCallLaterTest(ReactorTestCase):
     def test_callLater(self):
         self._later1Called = False
         self._later2Called = False
@@ -108,13 +116,14 @@ class ReactorTwoCallLaterTest(unittest.TestCase):
         self._called2 = self._reactor.seconds()
         self._reactor.stop()
 
-class ReactorCallFromThreadTest(unittest.TestCase):
+class ReactorCallFromThreadTest(ReactorTestCase):
     def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
+        super(ReactorCallFromThreadTest, self).setUp()
         self._mainThread = thread.get_ident()
 
     def tearDown(self):
         self._thread.join()
+        super(ReactorCallFromThreadTest, self).tearDown()
 
     def _newThreadRun(self):
         self.assertNotEqual(self._mainThread, thread.get_ident())
@@ -134,9 +143,9 @@ class ReactorCallFromThreadTest(unittest.TestCase):
         self._reactor.callWhenRunning(self._whenRunningCallback)
         self._reactor.run()
 
-class ReactorCallInThread(unittest.TestCase):
+class ReactorCallInThread(ReactorTestCase):
     def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
+        super(ReactorCallInThread, self).setUp()
         self._mainThread = thread.get_ident()
 
     def _fnCalledInThread(self, *args, **kwargs):
@@ -192,13 +201,13 @@ class Writer:
     def doWrite(self):
         self._callback(self._fd)
 
-class ReactorReaderWriterTest(unittest.TestCase):
+class ReactorReaderWriterTest(ReactorTestCase):
     def _set_nonblocking(self, fd):
         flags = fcntl.fcntl(fd, fcntl.F_GETFL)
         fcntl.fcntl(fd, fcntl.F_SETFL, flags | os.O_NONBLOCK)
 
     def setUp(self):
-        self._reactor = TornadoReactor(IOLoop())
+        super(ReactorReaderWriterTest, self).setUp()
         r, w = os.pipe()
         self._set_nonblocking(r)
         self._set_nonblocking(w)
@@ -207,6 +216,11 @@ class ReactorReaderWriterTest(unittest.TestCase):
         self._p1 = os.fdopen(r, "rb", 0)
         self._p2 = os.fdopen(w, "wb", 0)
 
+    def tearDown(self):
+        super(ReactorReaderWriterTest, self).tearDown()
+        self._p1.close()
+        self._p2.close()
+
     def _testReadWrite(self):
         """
         In this test the writer writes an 'x' to its fd. The reader
@@ -267,6 +281,106 @@ class ReactorReaderWriterTest(unittest.TestCase):
         self._reactor.callWhenRunning(self._testNoWriter)
         self._reactor.run()
 
+# Test various combinations of twisted and tornado http servers,
+# http clients, and event loop interfaces.
+class CompatibilityTests(unittest.TestCase):
+    def setUp(self):
+        self.io_loop = IOLoop()
+        self.reactor = TornadoReactor(self.io_loop)
+
+    def tearDown(self):
+        self.reactor.disconnectAll()
+        self.io_loop.close(all_fds=True)
+
+    def start_twisted_server(self):
+        class HelloResource(Resource):
+            isLeaf = True
+            def render_GET(self, request):
+                return "Hello from twisted!"
+        site = Site(HelloResource())
+        self.twisted_port = get_unused_port()
+        self.reactor.listenTCP(self.twisted_port, site, interface='127.0.0.1')
+
+    def start_tornado_server(self):
+        class HelloHandler(RequestHandler):
+            def get(self):
+                self.write("Hello from tornado!")
+        app = Application([('/', HelloHandler)],
+                          log_function=lambda x: None)
+        self.tornado_port = get_unused_port()
+        app.listen(self.tornado_port, address='127.0.0.1', io_loop=self.io_loop)
+
+    def run_ioloop(self):
+        self.stop_loop = self.io_loop.stop
+        self.io_loop.start()
+        self.reactor.fireSystemEvent('shutdown')
+
+    def run_reactor(self):
+        self.stop_loop = self.reactor.stop
+        self.stop = self.reactor.stop
+        self.reactor.run()
+
+    def tornado_fetch(self, url, runner):
+        responses = []
+        client = AsyncHTTPClient(self.io_loop)
+        def callback(response):
+            responses.append(response)
+            self.stop_loop()
+        client.fetch(url, callback=callback)
+        runner()
+        self.assertEqual(len(responses), 1)
+        responses[0].rethrow()
+        return responses[0]
+
+    def twisted_fetch(self, url, runner):
+        # http://twistedmatrix.com/documents/current/web/howto/client.html
+        chunks = []
+        client = Agent(self.reactor)
+        d = client.request('GET', url)
+        class Accumulator(Protocol):
+            def __init__(self, finished):
+                self.finished = finished
+            def dataReceived(self, data):
+                chunks.append(data)
+            def connectionLost(self, reason):
+                self.finished.callback(None)
+        def callback(response):
+            finished = Deferred()
+            response.deliverBody(Accumulator(finished))
+            return finished
+        d.addCallback(callback)
+        def shutdown(ignored):
+            self.stop_loop()
+        d.addBoth(shutdown)
+        runner()
+        self.assertTrue(chunks)
+        return ''.join(chunks)
+
+    def testTwistedServerTornadoClientIOLoop(self):
+        self.start_twisted_server()
+        response = self.tornado_fetch(
+            'http://localhost:%d' % self.twisted_port, self.run_ioloop)
+        self.assertEqual(response.body, 'Hello from twisted!')
+
+    def testTwistedServerTornadoClientReactor(self):
+        self.start_twisted_server()
+        response = self.tornado_fetch(
+            'http://localhost:%d' % self.twisted_port, self.run_reactor)
+        self.assertEqual(response.body, 'Hello from twisted!')
+
+    def testTornadoServerTwistedClientIOLoop(self):
+        self.start_tornado_server()
+        response = self.twisted_fetch(
+            'http://localhost:%d' % self.tornado_port, self.run_ioloop)
+        self.assertEqual(response, 'Hello from tornado!')
+
+    def testTornadoServerTwistedClientReactor(self):
+        self.start_tornado_server()
+        response = self.twisted_fetch(
+            'http://localhost:%d' % self.tornado_port, self.run_reactor)
+        self.assertEqual(response, 'Hello from tornado!')
+
+
 if twisted is None:
     del ReactorWhenRunningTest
     del ReactorCallLaterTest
@@ -274,39 +388,87 @@ if twisted is None:
     del ReactorCallFromThreadTest
     del ReactorCallInThread
     del ReactorReaderWriterTest
+    del CompatibilityTests
 else:
     # Import and run as much of twisted's test suite as possible.
     # This is unfortunately rather dependent on implementation details,
     # but there doesn't appear to be a clean all-in-one conformance test
     # suite for reactors.
+    #
     # This is a list of all test suites using the ReactorBuilder
-    # available in Twisted 11.0.0.  Tests that do not currently pass
-    # with the TornadoReactor are commented out.
-    twisted_tests = [
-        'twisted.internet.test.test_core.ObjectModelIntegrationTest',
-        #'twisted.internet.test.test_core.SystemEventTestsBuilder',
-        'twisted.internet.test.test_fdset.ReactorFDSetTestsBuilder',
-        #'twisted.internet.test.test_process.ProcessTestsBuilder',
-        #'twisted.internet.test.test_process.PTYProcessTestsBuilder',
-        #'twisted.internet.test.test_tcp.TCPClientTestsBuilder',
-        'twisted.internet.test.test_tcp.TCPPortTestsBuilder',
-        'twisted.internet.test.test_tcp.TCPConnectionTestsBuilder',
-        'twisted.internet.test.test_threads.ThreadTestsBuilder',
-        'twisted.internet.test.test_time.TimeTestsBuilder',
-        #'twisted.internet.test.test_tls.SSLClientTestsMixin',
-        'twisted.internet.test.test_udp.UDPServerTestsBuilder',
-        #'twisted.internet.test.test_unix.UNIXTestsBuilder',
-        #'twisted.internet.test.test_unix.UNIXDatagramTestsBuilder',
-        ]
-    for test_name in twisted_tests:
+    # available in Twisted 11.0.0 and 11.1.0 (and a blacklist of
+    # specific test methods to be disabled).
+    twisted_tests = {
+        'twisted.internet.test.test_core.ObjectModelIntegrationTest': [],
+        'twisted.internet.test.test_core.SystemEventTestsBuilder': [
+            'test_iterate',  # deliberately not supported
+            ],
+        'twisted.internet.test.test_fdset.ReactorFDSetTestsBuilder': [
+            "test_lostFileDescriptor",  # incompatible with epoll and kqueue
+            ],
+        'twisted.internet.test.test_process.ProcessTestsBuilder': [
+            # Doesn't work on python 2.5
+            'test_systemCallUninterruptedByChildExit',
+            # Doesn't clean up its temp files
+            'test_shebang',
+            ],
+        'twisted.internet.test.test_process.PTYProcessTestsBuilder': [
+            'test_systemCallUninterruptedByChildExit',
+            ],
+        'twisted.internet.test.test_tcp.TCPClientTestsBuilder': [],
+        'twisted.internet.test.test_tcp.TCPPortTestsBuilder': [],
+        'twisted.internet.test.test_tcp.TCPConnectionTestsBuilder': [],
+        'twisted.internet.test.test_tcp.WriteSequenceTests': [],
+        'twisted.internet.test.test_tcp.AbortConnectionTestCase': [],
+        'twisted.internet.test.test_threads.ThreadTestsBuilder': [],
+        'twisted.internet.test.test_time.TimeTestsBuilder': [],
+        # Extra third-party dependencies (pyOpenSSL)
+        #'twisted.internet.test.test_tls.SSLClientTestsMixin': [],
+        'twisted.internet.test.test_udp.UDPServerTestsBuilder': [],
+        'twisted.internet.test.test_unix.UNIXTestsBuilder': [
+            # Platform-specific.  These tests would be skipped automatically
+            # if we were running twisted's own test runner.
+            'test_connectToLinuxAbstractNamespace',
+            'test_listenOnLinuxAbstractNamespace',
+            ],
+        'twisted.internet.test.test_unix.UNIXDatagramTestsBuilder': [
+            'test_listenOnLinuxAbstractNamespace',
+            ],
+        'twisted.internet.test.test_unix.UNIXPortTestsBuilder': [],
+        }
+    for test_name, blacklist in twisted_tests.iteritems():
         try:
-            test = import_object(test_name)
+            test_class = import_object(test_name)
         except (ImportError, AttributeError):
             continue
-        class TornadoTest(test):
-            _reactors = ["tornado.platform.twisted._TestReactor"]
-        TornadoTest.__name__ = test.__name__
-        globals().update(TornadoTest.makeTestCaseClasses())
+        for test_func in blacklist:
+            if hasattr(test_class, test_func):
+                # The test_func may be defined in a mixin, so clobber
+                # it instead of delattr()
+                setattr(test_class, test_func, lambda self: None)
+        def make_test_subclass(test_class):
+            class TornadoTest(test_class):
+                _reactors = ["tornado.platform.twisted._TestReactor"]
+                def unbuildReactor(self, reactor):
+                    test_class.unbuildReactor(self, reactor)
+                    # Clean up file descriptors (especially epoll/kqueue
+                    # objects) eagerly instead of leaving them for the
+                    # GC.  Unfortunately we can't do this in reactor.stop
+                    # since twisted expects to be able to unregister
+                    # connections in a post-shutdown hook.
+                    reactor._io_loop.close(all_fds=True)
+            TornadoTest.__name__ = test_class.__name__
+            return TornadoTest
+        test_subclass = make_test_subclass(test_class)
+        globals().update(test_subclass.makeTestCaseClasses())
+
+    # Since we're not using twisted's test runner, it's tricky to get
+    # logging set up well.  Most of the time it's easiest to just
+    # leave it turned off, but while working on these tests you may want
+    # to uncomment one of the other lines instead.
+    log.defaultObserver.stop()
+    #import sys; log.startLogging(sys.stderr, setStdout=0)
+    #log.startLoggingWithObserver(log.PythonLoggingObserver().emit, setStdout=0)
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tox.ini b/tox.ini
index 30a78088d1076cddb920d0baaaab2b5288cee58c..88c8c37575d043a35fdd262c225c80b963b4accb 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -36,8 +36,10 @@ deps =
      MySQL-python
      pycurl
      simplejson
-     twisted==11.0.0
+     twisted>=11.1.0
 
+# py26-full deliberately runs an older version of twisted to ensure
+# we're still compatible with the oldest version we support.
 [testenv:py26-full]
 basepython = python2.6
 deps =
@@ -50,7 +52,7 @@ basepython = python2.7
 deps =
      MySQL-python
      pycurl
-     twisted==11.0.0
+     twisted>=11.1.0
 
 [testenv:py27-curl]
 # Same as py27-full, but runs the tests with curl_httpclient by default.
@@ -60,7 +62,7 @@ basepython = python2.7
 deps =
      MySQL-python
      pycurl
-     twisted==11.0.0
+     twisted>=11.0.0
 commands = python -m tornado.test.runtests --httpclient=tornado.curl_httpclient.CurlAsyncHTTPClient {posargs:}
 
 # No pypy-full yet: pycurl doesn't build with pypy, and installing
index aec6654b8ce9910a1892913b107d9274006d9d42..6fbbf1eef2818dc959996f6857ad276dd9a97c38 100644 (file)
@@ -65,8 +65,8 @@ Other modules
 
 * `SimpleAsyncHTTPClient` no longer hangs on ``HEAD`` requests,
   responses with no content, or empty ``POST``/``PUT`` response bodies.
-* `tornado.platform.twisted` compatibility has been improved.  However,
-  only Twisted version 11.0.0 is supported (and not 11.1.0).
+* `tornado.platform.twisted` compatibility has been significantly improved.
+  Twisted version 11.1.0 is now supported in addition to 11.0.0.
 * `tornado.web` now behaves better when given malformed ``Cookie`` headers
 * `RequestHandler.redirect` now has a ``status`` argument to send
   status codes other than 301 and 302.
index 5b107d46c0b9438166f9aaf367ac35d5e3e210c6..6a4d85416a0103d25f9e052db3c8db309ae5e8f8 100644 (file)
@@ -3,17 +3,43 @@
 
 .. module:: tornado.platform.twisted
 
-This module contains an implementation of the Twisted Reactor built
-on the Tornado IOLoop.  This lets you run applications and libraries
-written for Twisted in a Tornado application.  To use it, simply call
-`install` at the beginnging of the application::
+This module contains a Twisted reactor build on the Tornado IOLoop,
+which lets you run applications and libraries written for Twisted in a
+Tornado application.  To use it, simply call `install` at the
+beginning of the application::
 
     import tornado.platform.twisted
     tornado.platform.twisted.install()
-
     from twisted.internet import reactor
-    ...
+
+When the app is ready to start, call `IOLoop.instance().start()`
+instead of `reactor.run()`.  This will allow you to use a mixture of
+Twisted and Tornado code in the same process.
+
+It is also possible to create a non-global reactor by calling
+`tornado.platform.twisted.TornadoReactor(io_loop)`.  However, if
+the `IOLoop` and reactor are to be short-lived (such as those used in
+unit tests), additional cleanup may be required.  Specifically, it is
+recommended to call::
+
+    reactor.fireSystemEvent('shutdown')
+    reactor.disconnectAll()
+
+before closing the `IOLoop`.
+
+This module has been tested with Twisted versions 11.0.0 and 11.1.0.
 
 .. function:: install(io_loop=None)
 
-   Installs this package as the default Twisted reactor.
+Install this package as the default Twisted reactor.
+
+.. class:: TornadoReactor(io_loop=None)
+
+Twisted reactor built on the Tornado IOLoop.
+
+Since it is intented to be used in applications where the top-level
+event loop is ``io_loop.start()`` rather than ``reactor.run()``,
+it is implemented a little differently than other Twisted reactors.
+We override `mainLoop` instead of `doIteration` and must implement
+timed call functionality on top of `IOLoop.add_timeout` rather than
+using the implementation in `PosixReactorBase`.