Introduce the ClientsFactory
Define a new class ClientsFactory, which provides lazy loading an simple instantiation of clients by using partial to set defaults for all client parameters. This class will be used in upcoming patches to implement an interface that allows plugins to register their own service clients in the ServiceClients interface. It will also be used to implement lazy loading of clients for tempest own clients. Partially-implements: bp client-manager-refactor Change-Id: If2295d56a8e52ffce28d6a0f15517bc325aab010
This commit is contained in:
parent
1c984e4034
commit
73dd51dfe1
@ -14,6 +14,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import importlib
|
||||
import inspect
|
||||
|
||||
from tempest.lib import auth
|
||||
from tempest.lib import exceptions
|
||||
|
||||
@ -34,6 +38,88 @@ def available_modules():
|
||||
return tempest_modules()
|
||||
|
||||
|
||||
class ClientsFactory(object):
|
||||
"""Builds service clients for a service client module
|
||||
|
||||
This class implements the logic of feeding service client parameters
|
||||
to service clients from a specific module. It allows setting the
|
||||
parameters once and obtaining new instances of the clients without the
|
||||
need of passing any parameter.
|
||||
|
||||
ClientsFactory can be used directly, or consumed via the `ServiceClients`
|
||||
class, which manages the authorization part.
|
||||
"""
|
||||
# TODO(andreaf) This version includes ClientsFactory but it does not
|
||||
# use it yet in ServiceClients
|
||||
|
||||
def __init__(self, module_path, client_names, auth_provider, **kwargs):
|
||||
"""Initialises the client factory
|
||||
|
||||
:param module_path Path to module that includes all service clients.
|
||||
All service client classes must be exposed by a single module.
|
||||
If they are separated in different modules, defining __all__
|
||||
in the root module can help, similar to what is done by service
|
||||
clients in tempest.
|
||||
:param client_names List or set of names of the service client classes.
|
||||
:param auth_provider The auth provider used to initialise client.
|
||||
:param kwargs Parameters to be passed to all clients. Parameters values
|
||||
can be overwritten when clients are initialised, but parameters
|
||||
cannot be deleted.
|
||||
:raise ImportError if the specified module_path cannot be imported
|
||||
|
||||
Example:
|
||||
|
||||
>>> # Get credentials and an auth_provider
|
||||
>>> clients = ClientsFactory(
|
||||
>>> module_path='my_service.my_service_clients',
|
||||
>>> client_names=['ServiceClient1', 'ServiceClient2'],
|
||||
>>> auth_provider=auth_provider,
|
||||
>>> service='my_service',
|
||||
>>> region='region1')
|
||||
>>> my_api_client = clients.MyApiClient()
|
||||
>>> my_api_client_region2 = clients.MyApiClient(region='region2')
|
||||
|
||||
"""
|
||||
# Import the module. If it's not importable, the raised exception
|
||||
# provides good enough information about what happened
|
||||
_module = importlib.import_module(module_path)
|
||||
# If any of the classes is not in the module we fail
|
||||
for class_name in client_names:
|
||||
# TODO(andreaf) This always passes all parameters to all clients.
|
||||
# In future to allow clients to specify the list of parameters
|
||||
# that they accept based out of a list of standard ones.
|
||||
|
||||
# Obtain the class
|
||||
klass = self._get_class(_module, class_name)
|
||||
final_kwargs = copy.copy(kwargs)
|
||||
|
||||
# Set the function as an attribute of the factory
|
||||
setattr(self, class_name, self._get_partial_class(
|
||||
klass, auth_provider, final_kwargs))
|
||||
|
||||
@classmethod
|
||||
def _get_partial_class(cls, klass, auth_provider, kwargs):
|
||||
|
||||
# Define a function that returns a new class instance by
|
||||
# combining default kwargs with extra ones
|
||||
def partial_class(**later_kwargs):
|
||||
kwargs.update(later_kwargs)
|
||||
return klass(auth_provider=auth_provider, **kwargs)
|
||||
|
||||
return partial_class
|
||||
|
||||
@classmethod
|
||||
def _get_class(cls, module, class_name):
|
||||
klass = getattr(module, class_name, None)
|
||||
if not klass:
|
||||
msg = 'Invalid class name, %s is not found in %s'
|
||||
raise AttributeError(msg % (class_name, module))
|
||||
if not inspect.isclass(klass):
|
||||
msg = 'Expected a class, got %s of type %s instead'
|
||||
raise TypeError(msg % (klass, type(klass)))
|
||||
return klass
|
||||
|
||||
|
||||
class ServiceClients(object):
|
||||
"""Service client provider class
|
||||
|
||||
|
@ -13,15 +13,154 @@
|
||||
# the License.
|
||||
|
||||
import fixtures
|
||||
import mock
|
||||
import testtools
|
||||
import types
|
||||
|
||||
from tempest.lib import auth
|
||||
from tempest.lib import exceptions
|
||||
from tempest import service_clients
|
||||
from tempest.tests import base
|
||||
from tempest.tests.lib import fake_auth_provider
|
||||
from tempest.tests.lib import fake_credentials
|
||||
|
||||
|
||||
has_attribute = testtools.matchers.MatchesPredicateWithParams(
|
||||
lambda x, y: hasattr(x, y), '{0} does not have an attribute {1}')
|
||||
|
||||
|
||||
class TestClientsFactory(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestClientsFactory, self).setUp()
|
||||
self.classes = []
|
||||
|
||||
def _setup_fake_module(self, class_names=None, extra_dict=None):
|
||||
class_names = class_names or []
|
||||
fake_module = types.ModuleType('fake_service_client')
|
||||
_dict = {}
|
||||
# Add fake classes to the fake module
|
||||
for name in class_names:
|
||||
_dict[name] = type(name, (object,), {})
|
||||
# Store it for assertions
|
||||
self.classes.append(_dict[name])
|
||||
if extra_dict:
|
||||
_dict[extra_dict] = extra_dict
|
||||
fake_module.__dict__.update(_dict)
|
||||
fixture_importlib = self.useFixture(fixtures.MockPatch(
|
||||
'importlib.import_module', return_value=fake_module))
|
||||
return fixture_importlib.mock
|
||||
|
||||
def test___init___one_class(self):
|
||||
fake_partial = 'fake_partial'
|
||||
partial_mock = self.useFixture(fixtures.MockPatch(
|
||||
'tempest.service_clients.ClientsFactory._get_partial_class',
|
||||
return_value=fake_partial)).mock
|
||||
class_names = ['FakeServiceClient1']
|
||||
mock_importlib = self._setup_fake_module(class_names=class_names)
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
params = {'k1': 'v1', 'k2': 'v2'}
|
||||
factory = service_clients.ClientsFactory('fake_path', class_names,
|
||||
auth_provider, **params)
|
||||
# Assert module has been imported
|
||||
mock_importlib.assert_called_once_with('fake_path')
|
||||
# All attributes have been created
|
||||
for client in class_names:
|
||||
self.assertThat(factory, has_attribute(client))
|
||||
# Partial have been invoked correctly
|
||||
partial_mock.assert_called_once_with(
|
||||
self.classes[0], auth_provider, params)
|
||||
# Get the clients
|
||||
for name in class_names:
|
||||
self.assertEqual(fake_partial, getattr(factory, name))
|
||||
|
||||
def test___init___two_classes(self):
|
||||
fake_partial = 'fake_partial'
|
||||
partial_mock = self.useFixture(fixtures.MockPatch(
|
||||
'tempest.service_clients.ClientsFactory._get_partial_class',
|
||||
return_value=fake_partial)).mock
|
||||
class_names = ['FakeServiceClient1', 'FakeServiceClient2']
|
||||
mock_importlib = self._setup_fake_module(class_names=class_names)
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
params = {'k1': 'v1', 'k2': 'v2'}
|
||||
factory = service_clients.ClientsFactory('fake_path', class_names,
|
||||
auth_provider, **params)
|
||||
# Assert module has been imported
|
||||
mock_importlib.assert_called_once_with('fake_path')
|
||||
# All attributes have been created
|
||||
for client in class_names:
|
||||
self.assertThat(factory, has_attribute(client))
|
||||
# Partial have been invoked the right number of times
|
||||
partial_mock.call_count = len(class_names)
|
||||
# Get the clients
|
||||
for name in class_names:
|
||||
self.assertEqual(fake_partial, getattr(factory, name))
|
||||
|
||||
def test___init___no_module(self):
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
class_names = ['FakeServiceClient1', 'FakeServiceClient2']
|
||||
with testtools.ExpectedException(ImportError, '.*fake_module.*'):
|
||||
service_clients.ClientsFactory('fake_module', class_names,
|
||||
auth_provider)
|
||||
|
||||
def test___init___not_a_class(self):
|
||||
class_names = ['FakeServiceClient1', 'FakeServiceClient2']
|
||||
extended_class_names = class_names + ['not_really_a_class']
|
||||
self._setup_fake_module(
|
||||
class_names=class_names, extra_dict='not_really_a_class')
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
expected_msg = '.*not_really_a_class.*str.*'
|
||||
with testtools.ExpectedException(TypeError, expected_msg):
|
||||
service_clients.ClientsFactory('fake_module', extended_class_names,
|
||||
auth_provider)
|
||||
|
||||
def test___init___class_not_found(self):
|
||||
class_names = ['FakeServiceClient1', 'FakeServiceClient2']
|
||||
extended_class_names = class_names + ['not_really_a_class']
|
||||
self._setup_fake_module(class_names=class_names)
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
expected_msg = '.*not_really_a_class.*fake_service_client.*'
|
||||
with testtools.ExpectedException(AttributeError, expected_msg):
|
||||
service_clients.ClientsFactory('fake_module', extended_class_names,
|
||||
auth_provider)
|
||||
|
||||
def test__get_partial_class_no_later_kwargs(self):
|
||||
expected_fake_client = 'not_really_a_client'
|
||||
self._setup_fake_module(class_names=[])
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
params = {'k1': 'v1', 'k2': 'v2'}
|
||||
factory = service_clients.ClientsFactory(
|
||||
'fake_path', [], auth_provider, **params)
|
||||
klass_mock = mock.Mock(return_value=expected_fake_client)
|
||||
partial = factory._get_partial_class(klass_mock, auth_provider, params)
|
||||
# Class has not be initialised yet
|
||||
klass_mock.assert_not_called()
|
||||
# Use partial and assert on parameters
|
||||
client = partial()
|
||||
self.assertEqual(expected_fake_client, client)
|
||||
klass_mock.assert_called_once_with(auth_provider=auth_provider,
|
||||
**params)
|
||||
|
||||
def test__get_partial_class_later_kwargs(self):
|
||||
expected_fake_client = 'not_really_a_client'
|
||||
self._setup_fake_module(class_names=[])
|
||||
auth_provider = fake_auth_provider.FakeAuthProvider()
|
||||
params = {'k1': 'v1', 'k2': 'v2'}
|
||||
later_params = {'k2': 'v4', 'k3': 'v3'}
|
||||
factory = service_clients.ClientsFactory(
|
||||
'fake_path', [], auth_provider, **params)
|
||||
klass_mock = mock.Mock(return_value=expected_fake_client)
|
||||
partial = factory._get_partial_class(klass_mock, auth_provider, params)
|
||||
# Class has not be initialised yet
|
||||
klass_mock.assert_not_called()
|
||||
# Use partial and assert on parameters
|
||||
client = partial(**later_params)
|
||||
params.update(later_params)
|
||||
self.assertEqual(expected_fake_client, client)
|
||||
klass_mock.assert_called_once_with(auth_provider=auth_provider,
|
||||
**params)
|
||||
|
||||
|
||||
class TestServiceClients(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
Loading…
Reference in New Issue
Block a user