Merge "Add module for loading specific classes"
This commit is contained in:
commit
26d92b591d
116
nova/loadables.py
Normal file
116
nova/loadables.py
Normal file
@ -0,0 +1,116 @@
|
||||
# Copyright (c) 2011-2012 OpenStack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Generic Loadable class support.
|
||||
|
||||
Meant to be used by such things as scheduler filters and weights where we
|
||||
want to load modules from certain directories and find certain types of
|
||||
classes within those modules. Note that this is quite different than
|
||||
generic plugins and the pluginmanager code that exists elsewhere.
|
||||
|
||||
Usage:
|
||||
|
||||
Create a directory with an __init__.py with code such as:
|
||||
|
||||
class SomeLoadableClass(object):
|
||||
pass
|
||||
|
||||
|
||||
class MyLoader(nova.loadables.BaseLoader)
|
||||
def __init__(self):
|
||||
super(MyLoader, self).__init__(SomeLoadableClass)
|
||||
|
||||
If you create modules in the same directory and subclass SomeLoadableClass
|
||||
within them, MyLoader().get_all_classes() will return a list
|
||||
of such classes.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
from nova import exception
|
||||
from nova.openstack.common import importutils
|
||||
|
||||
|
||||
class BaseLoader(object):
|
||||
def __init__(self, loadable_cls_type):
|
||||
mod = sys.modules[self.__class__.__module__]
|
||||
self.path = mod.__path__[0]
|
||||
self.package = mod.__package__
|
||||
self.loadable_cls_type = loadable_cls_type
|
||||
|
||||
def _is_correct_class(self, obj):
|
||||
"""Return whether an object is a class of the correct type and
|
||||
is not prefixed with an underscore.
|
||||
"""
|
||||
return (inspect.isclass(obj) and
|
||||
(not obj.__name__.startswith('_')) and
|
||||
issubclass(obj, self.loadable_cls_type))
|
||||
|
||||
def _get_classes_from_module(self, module_name):
|
||||
"""Get the classes from a module that match the type we want."""
|
||||
classes = []
|
||||
module = importutils.import_module(module_name)
|
||||
for obj_name in dir(module):
|
||||
# Skip objects that are meant to be private.
|
||||
if obj_name.startswith('_'):
|
||||
continue
|
||||
itm = getattr(module, obj_name)
|
||||
if self._is_correct_class(itm):
|
||||
classes.append(itm)
|
||||
return classes
|
||||
|
||||
def get_all_classes(self):
|
||||
"""Get the classes of the type we want from all modules found
|
||||
in the directory that defines this class.
|
||||
"""
|
||||
classes = []
|
||||
for dirpath, dirnames, filenames in os.walk(self.path):
|
||||
relpath = os.path.relpath(dirpath, self.path)
|
||||
if relpath == '.':
|
||||
relpkg = ''
|
||||
else:
|
||||
relpkg = '.%s' % '.'.join(relpath.split(os.sep))
|
||||
for fname in filenames:
|
||||
root, ext = os.path.splitext(fname)
|
||||
if ext != '.py' or root == '__init__':
|
||||
continue
|
||||
module_name = "%s%s.%s" % (self.package, relpkg, root)
|
||||
mod_classes = self._get_classes_from_module(module_name)
|
||||
classes.extend(mod_classes)
|
||||
return classes
|
||||
|
||||
def get_matching_classes(self, loadable_class_names):
|
||||
"""Get loadable classes from a list of names. Each name can be
|
||||
a full module path or the full path to a method that returns
|
||||
classes to use. The latter behavior is useful to specify a method
|
||||
that returns a list of classes to use in a default case.
|
||||
"""
|
||||
classes = []
|
||||
for cls_name in loadable_class_names:
|
||||
obj = importutils.import_class(cls_name)
|
||||
if self._is_correct_class(obj):
|
||||
classes.append(obj)
|
||||
elif inspect.isfunction(obj):
|
||||
# Get list of classes from a function
|
||||
for cls in obj():
|
||||
classes.append(cls)
|
||||
else:
|
||||
error_str = 'Not a class of the correct type'
|
||||
raise exception.ClassNotFound(class_name=cls_name,
|
||||
exception=error_str)
|
||||
return classes
|
27
nova/tests/fake_loadables/__init__.py
Normal file
27
nova/tests/fake_loadables/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright 2012 OpenStack LLC. # All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Fakes For Loadable class handling.
|
||||
"""
|
||||
|
||||
from nova import loadables
|
||||
|
||||
|
||||
class FakeLoadable(object):
|
||||
pass
|
||||
|
||||
|
||||
class FakeLoader(loadables.BaseLoader):
|
||||
def __init__(self):
|
||||
super(FakeLoader, self).__init__(FakeLoadable)
|
44
nova/tests/fake_loadables/fake_loadable1.py
Normal file
44
nova/tests/fake_loadables/fake_loadable1.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright 2012 OpenStack LLC. # All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Fake Loadable subclasses module #1
|
||||
"""
|
||||
|
||||
from nova.tests import fake_loadables
|
||||
|
||||
|
||||
class FakeLoadableSubClass1(fake_loadables.FakeLoadable):
|
||||
pass
|
||||
|
||||
|
||||
class FakeLoadableSubClass2(fake_loadables.FakeLoadable):
|
||||
pass
|
||||
|
||||
|
||||
class _FakeLoadableSubClass3(fake_loadables.FakeLoadable):
|
||||
"""Classes beginning with '_' will be ignored."""
|
||||
pass
|
||||
|
||||
|
||||
class FakeLoadableSubClass4(object):
|
||||
"""Not a correct subclass."""
|
||||
|
||||
|
||||
def return_valid_classes():
|
||||
return [FakeLoadableSubClass1, FakeLoadableSubClass2]
|
||||
|
||||
|
||||
def return_invalid_classes():
|
||||
return [FakeLoadableSubClass1, _FakeLoadableSubClass3,
|
||||
FakeLoadableSubClass4]
|
39
nova/tests/fake_loadables/fake_loadable2.py
Normal file
39
nova/tests/fake_loadables/fake_loadable2.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2012 OpenStack LLC. # All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Fake Loadable subclasses module #2
|
||||
"""
|
||||
|
||||
from nova.tests import fake_loadables
|
||||
|
||||
|
||||
class FakeLoadableSubClass5(fake_loadables.FakeLoadable):
|
||||
pass
|
||||
|
||||
|
||||
class FakeLoadableSubClass6(fake_loadables.FakeLoadable):
|
||||
pass
|
||||
|
||||
|
||||
class _FakeLoadableSubClass7(fake_loadables.FakeLoadable):
|
||||
"""Classes beginning with '_' will be ignored."""
|
||||
pass
|
||||
|
||||
|
||||
class FakeLoadableSubClass8(BaseException):
|
||||
"""Not a correct subclass."""
|
||||
|
||||
|
||||
def return_valid_class():
|
||||
return [FakeLoadableSubClass6]
|
113
nova/tests/test_loadables.py
Normal file
113
nova/tests/test_loadables.py
Normal file
@ -0,0 +1,113 @@
|
||||
# Copyright 2012 OpenStack LLC. # All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Tests For Loadable class handling.
|
||||
"""
|
||||
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.tests import fake_loadables
|
||||
|
||||
|
||||
class LoadablesTestCase(test.TestCase):
|
||||
def setUp(self):
|
||||
super(LoadablesTestCase, self).setUp()
|
||||
self.fake_loader = fake_loadables.FakeLoader()
|
||||
# The name that we imported above for testing
|
||||
self.test_package = 'nova.tests.fake_loadables'
|
||||
|
||||
def test_loader_init(self):
|
||||
self.assertEqual(self.fake_loader.package, self.test_package)
|
||||
# Test the path of the module
|
||||
ending_path = '/' + self.test_package.replace('.', '/')
|
||||
self.assertTrue(self.fake_loader.path.endswith(ending_path))
|
||||
self.assertEqual(self.fake_loader.loadable_cls_type,
|
||||
fake_loadables.FakeLoadable)
|
||||
|
||||
def _compare_classes(self, classes, expected):
|
||||
class_names = [cls.__name__ for cls in classes]
|
||||
self.assertEqual(set(class_names), set(expected))
|
||||
|
||||
def test_get_all_classes(self):
|
||||
classes = self.fake_loader.get_all_classes()
|
||||
expected_class_names = ['FakeLoadableSubClass1',
|
||||
'FakeLoadableSubClass2',
|
||||
'FakeLoadableSubClass5',
|
||||
'FakeLoadableSubClass6']
|
||||
self._compare_classes(classes, expected_class_names)
|
||||
|
||||
def test_get_matching_classes(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.FakeLoadableSubClass1',
|
||||
prefix + '.fake_loadable2.FakeLoadableSubClass5']
|
||||
classes = self.fake_loader.get_matching_classes(test_classes)
|
||||
expected_class_names = ['FakeLoadableSubClass1',
|
||||
'FakeLoadableSubClass5']
|
||||
self._compare_classes(classes, expected_class_names)
|
||||
|
||||
def test_get_matching_classes_with_underscore(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.FakeLoadableSubClass1',
|
||||
prefix + '.fake_loadable2._FakeLoadableSubClass7']
|
||||
self.assertRaises(exception.ClassNotFound,
|
||||
self.fake_loader.get_matching_classes,
|
||||
test_classes)
|
||||
|
||||
def test_get_matching_classes_with_wrong_type1(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.FakeLoadableSubClass4',
|
||||
prefix + '.fake_loadable2.FakeLoadableSubClass5']
|
||||
self.assertRaises(exception.ClassNotFound,
|
||||
self.fake_loader.get_matching_classes,
|
||||
test_classes)
|
||||
|
||||
def test_get_matching_classes_with_wrong_type2(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.FakeLoadableSubClass1',
|
||||
prefix + '.fake_loadable2.FakeLoadableSubClass8']
|
||||
self.assertRaises(exception.ClassNotFound,
|
||||
self.fake_loader.get_matching_classes,
|
||||
test_classes)
|
||||
|
||||
def test_get_matching_classes_with_one_function(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.return_valid_classes',
|
||||
prefix + '.fake_loadable2.FakeLoadableSubClass5']
|
||||
classes = self.fake_loader.get_matching_classes(test_classes)
|
||||
expected_class_names = ['FakeLoadableSubClass1',
|
||||
'FakeLoadableSubClass2',
|
||||
'FakeLoadableSubClass5']
|
||||
self._compare_classes(classes, expected_class_names)
|
||||
|
||||
def test_get_matching_classes_with_two_functions(self):
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.return_valid_classes',
|
||||
prefix + '.fake_loadable2.return_valid_class']
|
||||
classes = self.fake_loader.get_matching_classes(test_classes)
|
||||
expected_class_names = ['FakeLoadableSubClass1',
|
||||
'FakeLoadableSubClass2',
|
||||
'FakeLoadableSubClass6']
|
||||
self._compare_classes(classes, expected_class_names)
|
||||
|
||||
def test_get_matching_classes_with_function_including_invalids(self):
|
||||
# When using a method, no checking is done on valid classes.
|
||||
prefix = self.test_package
|
||||
test_classes = [prefix + '.fake_loadable1.return_invalid_classes',
|
||||
prefix + '.fake_loadable2.return_valid_class']
|
||||
classes = self.fake_loader.get_matching_classes(test_classes)
|
||||
expected_class_names = ['FakeLoadableSubClass1',
|
||||
'_FakeLoadableSubClass3',
|
||||
'FakeLoadableSubClass4',
|
||||
'FakeLoadableSubClass6']
|
||||
self._compare_classes(classes, expected_class_names)
|
Loading…
Reference in New Issue
Block a user