diff --git a/nova/loadables.py b/nova/loadables.py new file mode 100644 index 000000000000..0c930267ef3b --- /dev/null +++ b/nova/loadables.py @@ -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 diff --git a/nova/tests/fake_loadables/__init__.py b/nova/tests/fake_loadables/__init__.py new file mode 100644 index 000000000000..824243347510 --- /dev/null +++ b/nova/tests/fake_loadables/__init__.py @@ -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) diff --git a/nova/tests/fake_loadables/fake_loadable1.py b/nova/tests/fake_loadables/fake_loadable1.py new file mode 100644 index 000000000000..58f9704b30e1 --- /dev/null +++ b/nova/tests/fake_loadables/fake_loadable1.py @@ -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] diff --git a/nova/tests/fake_loadables/fake_loadable2.py b/nova/tests/fake_loadables/fake_loadable2.py new file mode 100644 index 000000000000..3e365effc1e3 --- /dev/null +++ b/nova/tests/fake_loadables/fake_loadable2.py @@ -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] diff --git a/nova/tests/test_loadables.py b/nova/tests/test_loadables.py new file mode 100644 index 000000000000..6d16b9fa8b63 --- /dev/null +++ b/nova/tests/test_loadables.py @@ -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)