]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Extract configure logic from AsyncHTTPClient to a base class.
authorBen Darnell <ben@bendarnell.com>
Mon, 1 Oct 2012 05:19:59 +0000 (22:19 -0700)
committerBen Darnell <ben@bendarnell.com>
Mon, 1 Oct 2012 05:19:59 +0000 (22:19 -0700)
IOLoop now extends this base class as well, although no other
implementations are provided yet.  This does not include the
pseudo-singleton magic from AsyncHTTPClient.

tornado/httpclient.py
tornado/ioloop.py
tornado/test/util_test.py
tornado/util.py

index bf17fdebbe963640c235107680e6fd398a260bad..5dbc9bfc0d8c0e30aee7fb8699d0d8dcea9e8b66 100644 (file)
@@ -40,7 +40,7 @@ import weakref
 from tornado.escape import utf8
 from tornado import httputil
 from tornado.ioloop import IOLoop
-from tornado.util import import_object, bytes_type
+from tornado.util import import_object, bytes_type, Configurable
 
 
 class HTTPClient(object):
@@ -95,7 +95,7 @@ class HTTPClient(object):
         return response
 
 
-class AsyncHTTPClient(object):
+class AsyncHTTPClient(Configurable):
     """An non-blocking HTTP client.
 
     Example usage::
@@ -121,37 +121,31 @@ class AsyncHTTPClient(object):
     are deprecated.  The implementation subclass as well as arguments to
     its constructor can be set with the static method configure()
     """
-    _impl_class = None
-    _impl_kwargs = None
+    @classmethod
+    def configurable_base(cls):
+        return AsyncHTTPClient
+
+    @classmethod
+    def configurable_default(cls):
+        from tornado.simple_httpclient import SimpleAsyncHTTPClient
+        return SimpleAsyncHTTPClient
 
     @classmethod
     def _async_clients(cls):
-        assert cls is not AsyncHTTPClient, "should only be called on subclasses"
-        if not hasattr(cls, '_async_client_dict'):
-            cls._async_client_dict = weakref.WeakKeyDictionary()
-        return cls._async_client_dict
+        attr_name = '_async_client_dict_' + cls.__name__
+        if not hasattr(cls, attr_name):
+            setattr(cls, attr_name,  weakref.WeakKeyDictionary())
+        return getattr(cls, attr_name)
 
     def __new__(cls, io_loop=None, force_instance=False, **kwargs):
         io_loop = io_loop or IOLoop.instance()
-        args = {}
-        if cls is AsyncHTTPClient:
-            if cls._impl_class is None:
-                from tornado.simple_httpclient import SimpleAsyncHTTPClient
-                AsyncHTTPClient._impl_class = SimpleAsyncHTTPClient
-            impl = AsyncHTTPClient._impl_class
-            if cls._impl_kwargs:
-                args.update(cls._impl_kwargs)
-        else:
-            impl = cls
-        if io_loop in impl._async_clients() and not force_instance:
-            return impl._async_clients()[io_loop]
-        else:
-            instance = super(AsyncHTTPClient, cls).__new__(impl)
-            args.update(kwargs)
-            instance.initialize(io_loop, **args)
-            if not force_instance:
-                impl._async_clients()[io_loop] = instance
-            return instance
+        if io_loop in cls._async_clients() and not force_instance:
+            return cls._async_clients()[io_loop]
+        instance = super(AsyncHTTPClient, cls).__new__(cls, io_loop=io_loop,
+                                                       **kwargs)
+        if not force_instance:
+            cls._async_clients()[io_loop] = instance
+        return instance
 
     def close(self):
         """Destroys this http client, freeing any file descriptors used.
@@ -176,8 +170,8 @@ class AsyncHTTPClient(object):
         """
         raise NotImplementedError()
 
-    @staticmethod
-    def configure(impl, **kwargs):
+    @classmethod
+    def configure(cls, impl, **kwargs):
         """Configures the AsyncHTTPClient subclass to use.
 
         AsyncHTTPClient() actually creates an instance of a subclass.
@@ -196,21 +190,7 @@ class AsyncHTTPClient(object):
 
            AsyncHTTPClient.configure("tornado.curl_httpclient.CurlAsyncHTTPClient")
         """
-        if isinstance(impl, (unicode, bytes_type)):
-            impl = import_object(impl)
-        if impl is not None and not issubclass(impl, AsyncHTTPClient):
-            raise ValueError("Invalid AsyncHTTPClient implementation")
-        AsyncHTTPClient._impl_class = impl
-        AsyncHTTPClient._impl_kwargs = kwargs
-
-    @staticmethod
-    def _save_configuration():
-        return (AsyncHTTPClient._impl_class, AsyncHTTPClient._impl_kwargs)
-
-    @staticmethod
-    def _restore_configuration(saved):
-        AsyncHTTPClient._impl_class = saved[0]
-        AsyncHTTPClient._impl_kwargs = saved[1]
+        super(AsyncHTTPClient, cls).configure(impl, **kwargs)
 
 
 class HTTPRequest(object):
index 3e32be34e4fc0175320848d90711bd792f87f948..29e0edb6ced38c80e7a3da28fe52f6cb90acf5e9 100644 (file)
@@ -43,6 +43,7 @@ import traceback
 from tornado.concurrent import DummyFuture
 from tornado.log import app_log, gen_log
 from tornado import stack_context
+from tornado.util import Configurable
 
 try:
     import signal
@@ -57,7 +58,7 @@ except ImportError:
 from tornado.platform.auto import set_close_exec, Waker
 
 
-class IOLoop(object):
+class IOLoop(Configurable):
     """A level-triggered I/O loop.
 
     We use epoll (Linux) or kqueue (BSD and Mac OS X; requires python
@@ -96,6 +97,14 @@ class IOLoop(object):
         io_loop.start()
 
     """
+    @classmethod
+    def configurable_base(cls):
+        return IOLoop
+
+    @classmethod
+    def configurable_default(cls):
+        return IOLoop
+
     # Constants from the epoll module
     _EPOLLIN = 0x001
     _EPOLLPRI = 0x002
@@ -117,7 +126,7 @@ class IOLoop(object):
 
     _current = threading.local()
 
-    def __init__(self, impl=None):
+    def initialize(self, impl=None):
         self._impl = impl or _poll()
         if hasattr(self._impl, 'fileno'):
             set_close_exec(self._impl.fileno())
index 338c16a96c6f89c7b1a4145283f731ca145c6276..581c4d814e2cf7d5ab82a537ec29a03a5bfd406a 100644 (file)
@@ -1,7 +1,7 @@
 from __future__ import absolute_import, division, with_statement
 import sys
 
-from tornado.util import raise_exc_info
+from tornado.util import raise_exc_info, Configurable
 from tornado.test.util import unittest
 
 
@@ -24,3 +24,91 @@ class RaiseExcInfoTest(unittest.TestCase):
             self.fail("didn't get expected exception")
         except TwoArgException, e:
             self.assertIs(e, exc_info[1])
+
+class TestConfigurable(Configurable):
+    @classmethod
+    def configurable_base(cls):
+        return TestConfigurable
+
+    @classmethod
+    def configurable_default(cls):
+        return TestConfig1
+
+class TestConfig1(TestConfigurable):
+    def initialize(self, a=None):
+        self.a = a
+
+class TestConfig2(TestConfigurable):
+    def initialize(self, b=None):
+        self.b = b
+
+class ConfigurableTest(unittest.TestCase):
+    def setUp(self):
+        self.saved = TestConfigurable._save_configuration()
+
+    def tearDown(self):
+        TestConfigurable._restore_configuration(self.saved)
+
+    def checkSubclasses(self):
+        # no matter how the class is configured, it should always be
+        # possible to instantiate the subclasses directly
+        self.assertIsInstance(TestConfig1(), TestConfig1)
+        self.assertIsInstance(TestConfig2(), TestConfig2)
+
+        obj = TestConfig1(a=1)
+        self.assertEqual(obj.a, 1)
+        obj = TestConfig2(b=2)
+        self.assertEqual(obj.b, 2)
+
+    def test_default(self):
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig1)
+        self.assertIs(obj.a, None)
+
+        obj = TestConfigurable(a=1)
+        self.assertIsInstance(obj, TestConfig1)
+        self.assertEqual(obj.a, 1)
+
+        self.checkSubclasses()
+
+    def test_config_class(self):
+        TestConfigurable.configure(TestConfig2)
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig2)
+        self.assertIs(obj.b, None)
+
+        obj = TestConfigurable(b=2)
+        self.assertIsInstance(obj, TestConfig2)
+        self.assertEqual(obj.b, 2)
+
+        self.checkSubclasses()
+
+    def test_config_args(self):
+        TestConfigurable.configure(None, a=3)
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig1)
+        self.assertEqual(obj.a, 3)
+
+        obj = TestConfigurable(a=4)
+        self.assertIsInstance(obj, TestConfig1)
+        self.assertEqual(obj.a, 4)
+
+        self.checkSubclasses()
+        # args bound in configure don't apply when using the subclass directly
+        obj = TestConfig1()
+        self.assertIs(obj.a, None)
+
+    def test_config_class_args(self):
+        TestConfigurable.configure(TestConfig2, b=5)
+        obj = TestConfigurable()
+        self.assertIsInstance(obj, TestConfig2)
+        self.assertEqual(obj.b, 5)
+
+        obj = TestConfigurable(b=6)
+        self.assertIsInstance(obj, TestConfig2)
+        self.assertEqual(obj.b, 6)
+
+        self.checkSubclasses()
+        # args bound in configure don't apply when using the subclass directly
+        obj = TestConfig2()
+        self.assertIs(obj.b, None)
index 80cab89801bc76a188bcbf2fd54a6b3f4fb2b0da..9674708393a2d41182c0a84b7042059b1b7bd4b7 100644 (file)
@@ -96,6 +96,95 @@ def raise_exc_info(exc_info):
         # After 2to3: raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
 
 
+class Configurable(object):
+    """Base class for configurable interfaces.
+
+    A configurable interface is an (abstract) class whose constructor
+    acts as a factory function for one of its implementation subclasses.
+    The implementation subclass as well as optional keyword arguments to
+    its initializer can be set globally at runtime with `configure`.
+
+    By using the constructor as the factory method, the interface looks like
+    a normal class, ``isinstance()`` works as usual, etc.  This pattern
+    is most useful when the choice of implementation is likely to be a
+    global decision (e.g. when epoll is available, always use it instead of
+    select), or when a previously-monolithic class has been split into
+    specialized subclasses.
+
+    Configurable subclasses must define the class methods
+    `configurable_base` and `configurable_default`, and use the instance
+    method `initialize` instead of `__init__`.
+    """
+    __impl_class = None
+    __impl_kwargs = None
+
+    def __new__(cls, **kwargs):
+        base = cls.configurable_base()
+        args = {}
+        if cls is base:
+            if cls.__impl_class is None:
+                base.__impl_class = cls.configurable_default()
+            impl = base.__impl_class
+            if base.__impl_kwargs:
+                args.update(base.__impl_kwargs)
+        else:
+            impl = cls
+        args.update(kwargs)
+        instance = super(Configurable, cls).__new__(impl)
+        # initialize vs __init__ chosen for compatiblity with AsyncHTTPClient
+        # singleton magic.  If we get rid of that we can switch to __init__
+        # here too.
+        instance.initialize(**args)
+        return instance
+
+    @classmethod
+    def configurable_base(cls):
+        """Returns the base class of a configurable hierarchy.
+
+        This will normally return the class in which it is defined.
+        (which is *not* necessarily the same as the cls classmethod parameter).
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def configurable_default(cls):
+        """Returns the implementation class to be used if none is configured."""
+        raise NotImplementedError()
+
+    def initialize(self):
+        """Initialize a `Configurable` subclass instance.
+
+        Configurable classes should use `initialize` instead of `__init__`.
+        """
+
+    @classmethod
+    def configure(cls, impl, **kwargs):
+        """Sets the class to use when the base class is instantiated.
+
+        Keyword arguments will be saved and added to the arguments passed
+        to the constructor.  This can be used to set global defaults for
+        some parameters.
+        """
+        base = cls.configurable_base()
+        if isinstance(impl, (unicode, bytes_type)):
+            impl = import_object(impl)
+        if impl is not None and not issubclass(impl, cls):
+            raise ValueError("Invalid subclass of %s" % cls)
+        base.__impl_class = impl
+        base.__impl_kwargs = kwargs
+
+    @classmethod
+    def _save_configuration(cls):
+        base = cls.configurable_base()
+        return (base.__impl_class, base.__impl_kwargs)
+
+    @classmethod
+    def _restore_configuration(cls, saved):
+        base = cls.configurable_base()
+        base.__impl_class = saved[0]
+        base.__impl_kwargs = saved[1]
+
+
 def doctests():
     import doctest
     return doctest.DocTestSuite()