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):
return response
-class AsyncHTTPClient(object):
+class AsyncHTTPClient(Configurable):
"""An non-blocking HTTP client.
Example usage::
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.
"""
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.
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):
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
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
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
_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())
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
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)
# 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()