diff options
author | Doug Hellmann <doug.hellmann@gmail.com> | 2013-11-25 07:45:09 -0800 |
---|---|---|
committer | Doug Hellmann <doug.hellmann@gmail.com> | 2013-11-25 07:45:09 -0800 |
commit | 3e5c24f83aa4085b791bce0f336553f1a6029b5e (patch) | |
tree | f586c066e7796fca0e82edaf09d3098f73be50dd | |
parent | cc4e5c18dc5bc9d39adc2107342002c7239e14ae (diff) | |
parent | e2d86a8a0901e30db3697e6a71bc191a8cec4cbd (diff) | |
download | stevedore-3e5c24f83aa4085b791bce0f336553f1a6029b5e.tar.gz |
Merge pull request #29 from drocco-007/master
Test instance factories
-rw-r--r-- | stevedore/dispatch.py | 3 | ||||
-rw-r--r-- | stevedore/driver.py | 44 | ||||
-rw-r--r-- | stevedore/enabled.py | 2 | ||||
-rw-r--r-- | stevedore/extension.py | 53 | ||||
-rw-r--r-- | stevedore/hook.py | 8 | ||||
-rw-r--r-- | stevedore/named.py | 60 | ||||
-rw-r--r-- | stevedore/tests/test_test_manager.py | 278 |
7 files changed, 425 insertions, 23 deletions
diff --git a/stevedore/dispatch.py b/stevedore/dispatch.py index 6536ee9..2ff455d 100644 --- a/stevedore/dispatch.py +++ b/stevedore/dispatch.py @@ -142,6 +142,9 @@ class NameDispatchExtensionManager(DispatchExtensionManager): invoke_kwds=invoke_kwds, propagate_map_exceptions=propagate_map_exceptions, ) + + def _init_plugins(self, extensions): + super(NameDispatchExtensionManager, self)._init_plugins(extensions) self.by_name = dict((e.name, e) for e in self.extensions) def map(self, names, func, *args, **kwds): diff --git a/stevedore/driver.py b/stevedore/driver.py index ea2e122..56e408d 100644 --- a/stevedore/driver.py +++ b/stevedore/driver.py @@ -30,16 +30,48 @@ class DriverManager(NamedExtensionManager): invoke_args=invoke_args, invoke_kwds=invoke_kwds, ) + + @classmethod + def make_test_instance(cls, extension, namespace='TESTING', + propagate_map_exceptions=False): + """Construct a test DriverManager + + Test instances are passed a list of extensions to work from rather + than loading them from entry points. DriverManager test instances use + the name argument to determine which of the available_extensions should + be used as the driver. + + :param extension: Pre-configured Extension instances + :type extension: list of :class:`~stevedore.extension.Extension` + :param namespace: The namespace for the manager; used only for + identification since the extensions are passed in. + :type namespace: str + :param propagate_map_exceptions: Boolean controlling whether exceptions + are propagated up through the map call or whether they are logged + and then ignored + :type propagate_map_exceptions: bool + :return: The manager instance, initialized for testing + + """ + + o = super(DriverManager, cls).make_test_instance( + [extension], namespace=namespace, + propagate_map_exceptions=propagate_map_exceptions) + return o + + def _init_plugins(self, extensions, propagate_map_exceptions=False): + super(DriverManager, self)._init_plugins(extensions) + if not self.extensions: + name = self._names[0] raise RuntimeError('No %r driver found, looking for %r' % - (namespace, name)) + (self.namespace, name)) if len(self.extensions) > 1: + discovered_drivers = ','.join(e.entry_point_target + for e in self.extensions) + raise RuntimeError('Multiple %r drivers found: %s' % - (namespace, - ','.join('%s:%s' % (e.entry_point.module_name, - e.entry_point.attrs[0]) - for e in self.extensions)) - ) + (self.namespace, discovered_drivers)) def __call__(self, func, *args, **kwds): """Invokes func() for the single loaded extension. diff --git a/stevedore/enabled.py b/stevedore/enabled.py index 5cb0033..b6260b1 100644 --- a/stevedore/enabled.py +++ b/stevedore/enabled.py @@ -31,7 +31,7 @@ class EnabledExtensionManager(ExtensionManager): :param propagate_map_exceptions: Boolean controlling whether exceptions are propagated up through the map call or whether they are logged and then ignored - :type invoke_on_load: bool + :type propagate_map_exceptions: bool """ diff --git a/stevedore/extension.py b/stevedore/extension.py index 1ac87e8..2e29a95 100644 --- a/stevedore/extension.py +++ b/stevedore/extension.py @@ -34,6 +34,16 @@ class Extension(object): self.plugin = plugin self.obj = obj + @property + def entry_point_target(self): + """The module and attribute referenced by this extension's entry_point. + + :return: A string representation of the target of the entry point in + 'dotted.module:object' format. + """ + return '%s:%s' % (self.entry_point.module_name, + self.entry_point.attrs[0]) + class ExtensionManager(object): """Base class for all of the other managers. @@ -54,7 +64,7 @@ class ExtensionManager(object): :param propagate_map_exceptions: Boolean controlling whether exceptions are propagated up through the map call or whether they are logged and then ignored - :type invoke_on_load: bool + :type propagate_map_exceptions: bool """ @@ -63,11 +73,46 @@ class ExtensionManager(object): invoke_args=(), invoke_kwds={}, propagate_map_exceptions=False): + self._init_attributes( + namespace, propagate_map_exceptions=propagate_map_exceptions) + extensions = self._load_plugins(invoke_on_load, + invoke_args, + invoke_kwds) + self._init_plugins(extensions) + + @classmethod + def make_test_instance(cls, extensions, namespace='TESTING', + propagate_map_exceptions=False): + """Construct a test ExtensionManager + + Test instances are passed a list of extensions to work from rather + than loading them from entry points. + + :param extensions: Pre-configured Extension instances to use + :type extensions: list of :class:`~stevedore.extension.Extension` + :param namespace: The namespace for the manager; used only for + identification since the extensions are passed in. + :type namespace: str + :param propagate_map_exceptions: When calling map, controls whether + exceptions are propagated up through the map call or whether they + are logged and then ignored + :type propagate_map_exceptions: bool + :return: The manager instance, initialized for testing + + """ + + o = cls.__new__(cls) + o._init_attributes(namespace, + propagate_map_exceptions=propagate_map_exceptions) + o._init_plugins(extensions) + return o + + def _init_attributes(self, namespace, propagate_map_exceptions=False): self.namespace = namespace self.propagate_map_exceptions = propagate_map_exceptions - self.extensions = self._load_plugins(invoke_on_load, - invoke_args, - invoke_kwds) + + def _init_plugins(self, extensions): + self.extensions = extensions self._extensions_by_name = None ENTRY_POINT_CACHE = {} diff --git a/stevedore/hook.py b/stevedore/hook.py index c1cec7f..b7cdef8 100644 --- a/stevedore/hook.py +++ b/stevedore/hook.py @@ -23,7 +23,6 @@ class HookManager(NamedExtensionManager): def __init__(self, namespace, name, invoke_on_load=False, invoke_args=(), invoke_kwds={}): - self._name = name super(HookManager, self).__init__( namespace, [name], @@ -32,6 +31,13 @@ class HookManager(NamedExtensionManager): invoke_kwds=invoke_kwds, ) + def _init_attributes(self, namespace, names, name_order=False, + propagate_map_exceptions=False): + super(HookManager, self)._init_attributes( + namespace, names, + propagate_map_exceptions=propagate_map_exceptions) + self._name = names[0] + def __getitem__(self, name): """Return the named extensions. diff --git a/stevedore/named.py b/stevedore/named.py index e4cf857..b4f620c 100644 --- a/stevedore/named.py +++ b/stevedore/named.py @@ -28,24 +28,62 @@ class NamedExtensionManager(ExtensionManager): :param propagate_map_exceptions: Boolean controlling whether exceptions are propagated up through the map call or whether they are logged and then ignored - :type invoke_on_load: bool + :type propagate_map_exceptions: bool + """ def __init__(self, namespace, names, invoke_on_load=False, invoke_args=(), invoke_kwds={}, name_order=False, propagate_map_exceptions=False): + self._init_attributes( + namespace, names, name_order=name_order, + propagate_map_exceptions=propagate_map_exceptions) + extensions = self._load_plugins(invoke_on_load, + invoke_args, + invoke_kwds) + self._init_plugins(extensions) + + @classmethod + def make_test_instance(cls, extensions, namespace='TESTING', + propagate_map_exceptions=False): + """Construct a test NamedExtensionManager + + Test instances are passed a list of extensions to use rather than + loading them from entry points. + + :param extensions: Pre-configured Extension instances + :type extensions: list of :class:`~stevedore.extension.Extension` + :param namespace: The namespace for the manager; used only for + identification since the extensions are passed in. + :type namespace: str + :param propagate_map_exceptions: Boolean controlling whether exceptions + are propagated up through the map call or whether they are logged + and then ignored + :type propagate_map_exceptions: bool + :return: The manager instance, initialized for testing + + """ + + o = cls.__new__(cls) + names = [e.name for e in extensions] + o._init_attributes(namespace, names, + propagate_map_exceptions=propagate_map_exceptions) + o._init_plugins(extensions) + return o + + def _init_attributes(self, namespace, names, name_order=False, + propagate_map_exceptions=False): + super(NamedExtensionManager, self)._init_attributes( + namespace, propagate_map_exceptions=propagate_map_exceptions) + self._names = names - super(NamedExtensionManager, self).__init__( - namespace, - invoke_on_load=invoke_on_load, - invoke_args=invoke_args, - invoke_kwds=invoke_kwds, - propagate_map_exceptions=propagate_map_exceptions, - ) + self._name_order = name_order + + def _init_plugins(self, extensions): + super(NamedExtensionManager, self)._init_plugins(extensions) - if name_order: - ext_map = dict((x.name, x) for x in self.extensions) - self.extensions = [ext_map[n] for n in names] + if self._name_order: + self.extensions = [self[n] for n in self._names] def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds): # Check the name before going any further to prevent diff --git a/stevedore/tests/test_test_manager.py b/stevedore/tests/test_test_manager.py new file mode 100644 index 0000000..aa995b2 --- /dev/null +++ b/stevedore/tests/test_test_manager.py @@ -0,0 +1,278 @@ +from mock import Mock, sentinel +from nose.tools import raises +from stevedore import (ExtensionManager, NamedExtensionManager, HookManager, + DriverManager, EnabledExtensionManager) +from stevedore.dispatch import (DispatchExtensionManager, + NameDispatchExtensionManager) +from stevedore.extension import Extension + + +test_extension = Extension('test_extension', None, None, None) +test_extension2 = Extension('another_one', None, None, None) + +mock_entry_point = Mock(module_name='test.extension', attrs=['obj']) +a_driver = Extension('test_driver', mock_entry_point, sentinel.driver_plugin, + sentinel.driver_obj) + + +# base ExtensionManager + +def test_instance_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = ExtensionManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +def test_instance_should_have_default_namespace(): + em = ExtensionManager.make_test_instance([]) + + assert em.namespace + + +def test_instance_should_use_supplied_namespace(): + namespace = 'testing.1.2.3' + em = ExtensionManager.make_test_instance([], namespace=namespace) + + assert namespace == em.namespace + + +def test_extension_name_should_be_listed(): + em = ExtensionManager.make_test_instance([test_extension]) + + assert test_extension.name in em.names() + + +def test_iterator_should_yield_extension(): + em = ExtensionManager.make_test_instance([test_extension]) + + assert test_extension == next(iter(em)) + + +def test_manager_should_allow_name_access(): + em = ExtensionManager.make_test_instance([test_extension]) + + assert test_extension == em[test_extension.name] + + +def test_manager_should_call(): + em = ExtensionManager.make_test_instance([test_extension]) + func = Mock() + + em.map(func) + + func.assert_called_once_with(test_extension) + + +def test_manager_should_call_all(): + em = ExtensionManager.make_test_instance([test_extension2, + test_extension]) + func = Mock() + + em.map(func) + + func.assert_any_call(test_extension2) + func.assert_any_call(test_extension) + + +def test_manager_return_values(): + def mapped(ext, *args, **kwds): + return ext.name + + em = ExtensionManager.make_test_instance([test_extension2, + test_extension]) + results = em.map(mapped) + assert sorted(results) == ['another_one', 'test_extension'] + + +def test_manager_should_eat_exceptions(): + em = ExtensionManager.make_test_instance([test_extension]) + + func = Mock(side_effect=RuntimeError('hard coded error')) + + results = em.map(func, 1, 2, a='A', b='B') + assert results == [] + + +@raises(RuntimeError) +def test_manager_should_propagate_exceptions(): + em = ExtensionManager.make_test_instance([test_extension], + propagate_map_exceptions=True) + func = Mock(side_effect=RuntimeError('hard coded error')) + + em.map(func, 1, 2, a='A', b='B') + + +# NamedExtensionManager + +def test_named_manager_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = NamedExtensionManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +def test_named_manager_should_have_default_namespace(): + em = NamedExtensionManager.make_test_instance([]) + + assert em.namespace + + +def test_named_manager_should_use_supplied_namespace(): + namespace = 'testing.1.2.3' + em = NamedExtensionManager.make_test_instance([], namespace=namespace) + + assert namespace == em.namespace + + +def test_named_manager_should_populate_names(): + extensions = [test_extension, test_extension2] + em = NamedExtensionManager.make_test_instance(extensions) + + assert ['test_extension', 'another_one'] == em.names() + + +# HookManager + +def test_hook_manager_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = HookManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +def test_hook_manager_should_be_first_extension_name(): + extensions = [test_extension, test_extension2] + em = HookManager.make_test_instance(extensions) + + # This will raise KeyError if the names don't match + assert em[test_extension.name] + + +def test_hook_manager_should_have_default_namespace(): + em = HookManager.make_test_instance([test_extension]) + + assert em.namespace + + +def test_hook_manager_should_use_supplied_namespace(): + namespace = 'testing.1.2.3' + em = HookManager.make_test_instance([test_extension], namespace=namespace) + + assert namespace == em.namespace + + +def test_hook_manager_should_return_named_extensions(): + hook1 = Extension('captain', None, None, None) + hook2 = Extension('captain', None, None, None) + + em = HookManager.make_test_instance([hook1, hook2]) + + assert [hook1, hook2] == em['captain'] + + +# DriverManager + +def test_driver_manager_should_use_supplied_extension(): + em = DriverManager.make_test_instance(a_driver) + + assert [a_driver] == em.extensions + + +def test_driver_manager_should_have_default_namespace(): + em = DriverManager.make_test_instance(a_driver) + + assert em.namespace + + +def test_driver_manager_should_use_supplied_namespace(): + namespace = 'testing.1.2.3' + em = DriverManager.make_test_instance(a_driver, namespace=namespace) + + assert namespace == em.namespace + + +def test_instance_should_use_driver_name(): + em = DriverManager.make_test_instance(a_driver) + + assert ['test_driver'] == em.names() + + +def test_instance_call(): + def invoke(ext, *args, **kwds): + return ext.name, args, kwds + + em = DriverManager.make_test_instance(a_driver) + result = em(invoke, 'a', b='C') + + assert result == ('test_driver', ('a',), {'b': 'C'}) + + +def test_instance_driver_property(): + em = DriverManager.make_test_instance(a_driver) + + assert sentinel.driver_obj == em.driver + + +# EnabledExtensionManager + +def test_enabled_instance_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = EnabledExtensionManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +# DispatchExtensionManager + +def test_dispatch_instance_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = DispatchExtensionManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +def test_dispatch_map_should_invoke_filter_for_extensions(): + em = DispatchExtensionManager.make_test_instance([test_extension, + test_extension2]) + + filter_func = Mock(return_value=False) + + args = ('A',) + kw = {'big': 'Cheese'} + + em.map(filter_func, None, *args, **kw) + + filter_func.assert_any_call(test_extension, *args, **kw) + filter_func.assert_any_call(test_extension2, *args, **kw) + + +# NameDispatchExtensionManager + +def test_name_dispatch_instance_should_use_supplied_extensions(): + extensions = [test_extension, test_extension2] + em = NameDispatchExtensionManager.make_test_instance(extensions) + + assert extensions == em.extensions + + +def test_name_dispatch_instance_should_build_extension_name_map(): + extensions = [test_extension, test_extension2] + em = NameDispatchExtensionManager.make_test_instance(extensions) + + assert test_extension == em.by_name[test_extension.name] + assert test_extension2 == em.by_name[test_extension2.name] + + +def test_named_dispatch_map_should_invoke_filter_for_extensions(): + em = NameDispatchExtensionManager.make_test_instance([test_extension, + test_extension2]) + + func = Mock() + + args = ('A',) + kw = {'BIGGER': 'Cheese'} + + em.map(['test_extension'], func, *args, **kw) + + func.assert_called_once_with(test_extension, *args, **kw) |