Repository: tornadoweb/tornado
Pull request #2062 (2017-05-27 23:18:17)

ioloop: Make PollIOLoop separately configurable
This makes it possible to construct a PollIOLoop even when the default
IOLoop is configured to something else.


Patch:
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index 5997cef6..f4d46420 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -249,15 +249,7 @@ class IOLoop(Configurable):
 
     @classmethod
     def configurable_default(cls):
-        if hasattr(select, "epoll"):
-            from tornado.platform.epoll import EPollIOLoop
-            return EPollIOLoop
-        if hasattr(select, "kqueue"):
-            # Python 2.6+ on BSD or Mac
-            from tornado.platform.kqueue import KQueueIOLoop
-            return KQueueIOLoop
-        from tornado.platform.select import SelectIOLoop
-        return SelectIOLoop
+        return PollIOLoop
 
     def initialize(self, make_current=None):
         if make_current is None:
@@ -722,6 +714,22 @@ class PollIOLoop(IOLoop):
                          lambda fd, events: self._waker.consume(),
                          self.READ)
 
+    @classmethod
+    def configurable_base(cls):
+        return PollIOLoop
+
+    @classmethod
+    def configurable_default(cls):
+        if hasattr(select, "epoll"):
+            from tornado.platform.epoll import EPollIOLoop
+            return EPollIOLoop
+        if hasattr(select, "kqueue"):
+            # Python 2.6+ on BSD or Mac
+            from tornado.platform.kqueue import KQueueIOLoop
+            return KQueueIOLoop
+        from tornado.platform.select import SelectIOLoop
+        return SelectIOLoop
+
     def close(self, all_fds=False):
         self._closing = True
         self.remove_handler(self._waker.fileno())
diff --git a/tornado/platform/twisted.py b/tornado/platform/twisted.py
index 79608e5f..7c337e8a 100644
--- a/tornado/platform/twisted.py
+++ b/tornado/platform/twisted.py
@@ -382,6 +382,8 @@ class _FD(object):
             self.handler(self.fileobj, tornado.ioloop.IOLoop.ERROR)
             self.lost = True
 
+    writeConnectionLost = readConnectionLost = connectionLost
+
     def logPrefix(self):
         return ''
 
diff --git a/tornado/util.py b/tornado/util.py
index 981b94c8..bfd80beb 100644
--- a/tornado/util.py
+++ b/tornado/util.py
@@ -286,6 +286,9 @@ class Configurable(object):
         else:
             impl = cls
         init_kwargs.update(kwargs)
+        if impl.configurable_base() is not base:
+            # The impl class is itself configurable, so recurse.
+            return impl(*args, **init_kwargs)
         instance = super(Configurable, cls).__new__(impl)
         # initialize vs __init__ chosen for compatibility with AsyncHTTPClient
         # singleton magic.  If we get rid of that we can switch to __init__
@@ -343,7 +346,10 @@ class Configurable(object):
         # type: () -> type
         """Returns the currently configured class."""
         base = cls.configurable_base()
-        if cls.__impl_class is None:
+        # Manually mangle the private name to see whether this base
+        # has been configured (and not another base higher in the
+        # hierarchy).
+        if base.__dict__.get('_Configurable__impl_class') is None:
             base.__impl_class = cls.configurable_default()
         return base.__impl_class
 


Test patch:
diff --git a/tornado/test/twisted_test.py b/tornado/test/twisted_test.py
index 10afebb7..4b88eca8 100644
--- a/tornado/test/twisted_test.py
+++ b/tornado/test/twisted_test.py
@@ -32,9 +32,8 @@ from tornado.escape import utf8
 from tornado import gen
 from tornado.httpclient import AsyncHTTPClient
 from tornado.httpserver import HTTPServer
-from tornado.ioloop import IOLoop
+from tornado.ioloop import IOLoop, PollIOLoop
 from tornado.platform.auto import set_close_exec
-from tornado.platform.select import SelectIOLoop
 from tornado.testing import bind_unused_port
 from tornado.test.util import unittest
 from tornado.util import import_object, PY3
@@ -690,7 +689,7 @@ if have_twisted:
 
 if have_twisted:
     class LayeredTwistedIOLoop(TwistedIOLoop):
-        """Layers a TwistedIOLoop on top of a TornadoReactor on a SelectIOLoop.
+        """Layers a TwistedIOLoop on top of a TornadoReactor on a PollIOLoop.
 
         This is of course silly, but is useful for testing purposes to make
         sure we're implementing both sides of the various interfaces
@@ -698,10 +697,7 @@ if have_twisted:
         of the whole stack.
         """
         def initialize(self, **kwargs):
-            # When configured to use LayeredTwistedIOLoop we can't easily
-            # get the next-best IOLoop implementation, so use the lowest common
-            # denominator.
-            self.real_io_loop = SelectIOLoop(make_current=False)  # type: ignore
+            self.real_io_loop = PollIOLoop(make_current=False)  # type: ignore
             reactor = self.real_io_loop.run_sync(gen.coroutine(TornadoReactor))
             super(LayeredTwistedIOLoop, self).initialize(reactor=reactor, **kwargs)
             self.add_callback(self.make_current)
diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py
index 459cb9c3..48924d44 100644
--- a/tornado/test/util_test.py
+++ b/tornado/test/util_test.py
@@ -58,12 +58,35 @@ class TestConfig2(TestConfigurable):
         self.pos_arg = pos_arg
 
 
+class TestConfig3(TestConfigurable):
+    # TestConfig3 is a configuration option that is itself configurable.
+    @classmethod
+    def configurable_base(cls):
+        return TestConfig3
+
+    @classmethod
+    def configurable_default(cls):
+        return TestConfig3A
+
+
+class TestConfig3A(TestConfig3):
+    def initialize(self, a=None):
+        self.a = a
+
+
+class TestConfig3B(TestConfig3):
+    def initialize(self, b=None):
+        self.b = b
+
+
 class ConfigurableTest(unittest.TestCase):
     def setUp(self):
         self.saved = TestConfigurable._save_configuration()
+        self.saved3 = TestConfig3._save_configuration()
 
     def tearDown(self):
         TestConfigurable._restore_configuration(self.saved)
+        TestConfig3._restore_configuration(self.saved3)
 
     def checkSubclasses(self):
         # no matter how the class is configured, it should always be
@@ -131,6 +154,39 @@ class ConfigurableTest(unittest.TestCase):
         obj = TestConfig2()
         self.assertIs(obj.b, None)
 
+    def test_config_multi_level(self):
+        TestConfigurable.configure(TestConfig3, a=1)
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig3A)
+        self.assertEqual(obj.a, 1)
+
+        TestConfigurable.configure(TestConfig3)
+        TestConfig3.configure(TestConfig3B, b=2)
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig3B)
+        self.assertEqual(obj.b, 2)
+
+    def test_config_inner_level(self):
+        # The inner level can be used even when the outer level
+        # doesn't point to it.
+        obj = TestConfig3()
+        self.assertIsInstance(obj, TestConfig3A)
+
+        TestConfig3.configure(TestConfig3B)
+        obj = TestConfig3()
+        self.assertIsInstance(obj, TestConfig3B)
+
+        # Configuring the base doesn't configure the inner.
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig1)
+        TestConfigurable.configure(TestConfig2)
+
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig2)
+
+        obj = TestConfig3()
+        self.assertIsInstance(obj, TestConfig3B)
+
 
 class UnicodeLiteralTest(unittest.TestCase):
     def test_unicode_escapes(self):