Permalink
Browse files

Extract configure logic from AsyncHTTPClient to a base class.

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.
  • Loading branch information...
1 parent 13a6238 commit 91c5eb83f9843298c53f55d27aa7744da2a56901 @bdarnell bdarnell committed Oct 1, 2012
Showing with 213 additions and 47 deletions.
  1. +24 −44 tornado/httpclient.py
  2. +11 −2 tornado/ioloop.py
  3. +89 −1 tornado/test/util_test.py
  4. +89 −0 tornado/util.py
View
68 tornado/httpclient.py
@@ -40,7 +40,7 @@
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 @@ def callback(response):
return response
-class AsyncHTTPClient(object):
+class AsyncHTTPClient(Configurable):
"""An non-blocking HTTP client.
Example usage::
@@ -121,37 +121,31 @@ def handle_request(response):
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 @@ def fetch(self, request, callback, **kwargs):
"""
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 @@ def configure(impl, **kwargs):
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):
View
13 tornado/ioloop.py
@@ -43,6 +43,7 @@
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 @@
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 @@ def connection_ready(sock, fd, events):
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 @@ def connection_ready(sock, fd, events):
_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())
View
90 tornado/test/util_test.py
@@ -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 @@ def __init__(self, a, b):
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)
View
89 tornado/util.py
@@ -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()

0 comments on commit 91c5eb8

Please sign in to comment.