From: Ben Darnell Date: Mon, 1 Oct 2012 05:19:59 +0000 (-0700) Subject: Extract configure logic from AsyncHTTPClient to a base class. X-Git-Tag: v3.0.0~251 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=91c5eb83f9843298c53f55d27aa7744da2a56901;p=thirdparty%2Ftornado.git 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. --- diff --git a/tornado/httpclient.py b/tornado/httpclient.py index bf17fdebb..5dbc9bfc0 100644 --- a/tornado/httpclient.py +++ b/tornado/httpclient.py @@ -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): diff --git a/tornado/ioloop.py b/tornado/ioloop.py index 3e32be34e..29e0edb6c 100644 --- a/tornado/ioloop.py +++ b/tornado/ioloop.py @@ -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()) diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py index 338c16a96..581c4d814 100644 --- a/tornado/test/util_test.py +++ b/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 @@ 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) diff --git a/tornado/util.py b/tornado/util.py index 80cab8980..967470839 100644 --- a/tornado/util.py +++ b/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()