From: Ben Darnell Date: Sat, 27 May 2017 23:10:45 +0000 (-0400) Subject: util: Enable multiple configurable bases in one hierarchy X-Git-Tag: v5.0.0~79^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6453ce8f2e553d5a1f393975daa89bc5a21e70f6;p=thirdparty%2Ftornado.git util: Enable multiple configurable bases in one hierarchy This allows a mid-level class like PollIOLoop to have its own configurability. --- diff --git a/tornado/test/util_test.py b/tornado/test/util_test.py index 459cb9c32..48924d44b 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): diff --git a/tornado/util.py b/tornado/util.py index 981b94c8e..bfd80beb6 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