diff --git a/monasca_common/simport/__init__.py b/monasca_common/simport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/monasca_common/simport/simport.py b/monasca_common/simport/simport.py new file mode 100644 index 00000000..a829f1a9 --- /dev/null +++ b/monasca_common/simport/simport.py @@ -0,0 +1,107 @@ +# Copyright 2014 - Dark Secret Software Inc. +# 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. + +import logging +import os +import os.path +import sys + + +LOG = logging.getLogger(__name__) + + +class MissingModule(Exception): + pass + + +class BadDirectory(Exception): + pass + + +class MissingMethodOrFunction(Exception): + pass + + +class ImportFailed(Exception): + pass + + +def _get_module(target): + """Import a named class, module, method or function. + + Accepts these formats: + ".../file/path|module_name:Class.method" + ".../file/path|module_name:Class" + ".../file/path|module_name:function" + "module_name:Class" + "module_name:function" + "module_name:Class.function" + + If a fully qualified directory is specified, it implies the + directory is not already on the Python Path, in which case + it will be added. + + For example, if I import /home/foo (and + /home/foo is not in the python path) as + "/home/foo|mycode:MyClass.mymethod" + then /home/foo will be added to the python path and + the module loaded as normal. + """ + + filepath, sep, namespace = target.rpartition('|') + if sep and not filepath: + raise BadDirectory("Path to file not supplied.") + + module, sep, class_or_function = namespace.rpartition(':') + if (sep and not module) or (filepath and not module): + raise MissingModule("Need a module path for %s (%s)" % + (namespace, target)) + + if filepath and filepath not in sys.path: + if not os.path.isdir(filepath): + raise BadDirectory("No such directory: '%s'" % filepath) + sys.path.append(filepath) + + if not class_or_function: + raise MissingMethodOrFunction( + "No Method or Function specified in '%s'" % target) + + if module: + try: + __import__(module) + except ImportError as e: + raise ImportFailed("Failed to import '%s'. " + "Error: %s" % (module, e)) + + klass, sep, function = class_or_function.rpartition('.') + return module, klass, function + + +def load(target, source_module=None): + """Get the actual implementation of the target.""" + module, klass, function = _get_module(target) + if not module and source_module: + module = source_module + if not module: + raise MissingModule( + "No module name supplied or source_module provided.") + actual_module = sys.modules[module] + if not klass: + return getattr(actual_module, function) + + class_object = getattr(actual_module, klass) + if function: + return getattr(class_object, function) + return class_object diff --git a/monasca_common/tests/external/__init__.py b/monasca_common/tests/external/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/monasca_common/tests/external/externalmodule.py b/monasca_common/tests/external/externalmodule.py new file mode 100644 index 00000000..27d071c3 --- /dev/null +++ b/monasca_common/tests/external/externalmodule.py @@ -0,0 +1,31 @@ +# Copyright (c) 2014 Dark Secret Software Inc. +# +# 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. + + +class Foo(object): + def method_a(self, a, b, c, d): + pass + + +class Blah(Foo): + def method_a(self, a, b, c, d): + pass + + def method_b(self, a, b, c, e): + pass + + +def function_a(a, b, c, d): + pass diff --git a/monasca_common/tests/localmodule.py b/monasca_common/tests/localmodule.py new file mode 100644 index 00000000..2b6993c1 --- /dev/null +++ b/monasca_common/tests/localmodule.py @@ -0,0 +1,31 @@ +# Copyright (c) 2014 Dark Secret Software Inc. +# +# 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. + + +class Foo(object): + def method_a(self, a, b, c, d): + pass + + +class Blah(Foo): + def method_a(self, a, b, c, d): + pass + + def method_b(self, a, b, c, e): + pass + + +def function_a(a, b, c): + return (a, b, c) diff --git a/monasca_common/tests/test_simport.py b/monasca_common/tests/test_simport.py new file mode 100755 index 00000000..7d0cbeda --- /dev/null +++ b/monasca_common/tests/test_simport.py @@ -0,0 +1,106 @@ +# Copyright (c) 2014 Dark Secret Software Inc. +# +# 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. + +import sys +import unittest + +import monasca_common.simport.simport as simport + + +# Internal functions and classes. +def dummy_function(): + pass + + +class DummyClass(object): + def method_a(self): + pass + + +class LocalClass(object): + def my_method(self): + pass + + +class TestSimport(unittest.TestCase): + def test_bad_targets(self): + self.assertRaises(simport.BadDirectory, simport._get_module, + "|foo.Class") + + self.assertRaises(simport.MissingModule, simport._get_module, + "missing|") + + self.assertRaises(simport.MissingModule, simport._get_module, + "simport_tests|") + self.assertRaises(simport.MissingModule, simport._get_module, + "simport_tests|Foo") + self.assertRaises(simport.BadDirectory, simport._get_module, + "/does/not/exist|foo:Class") + + self.assertFalse("AnyModuleName" in sys.modules) + self.assertRaises(simport.MissingMethodOrFunction, simport._get_module, + "tests|AnyModuleName:") + self.assertFalse("AnyModuleName" in sys.modules) + + def test_good_external_targets(self): + self.assertEqual(("localmodule", "Foo", "method_a"), + simport._get_module("tests|" + "localmodule:Foo.method_a")) + + self.assertRaises(simport.ImportFailed, simport._get_module, + "tests|that_module:function_a") + + def test_bad_load(self): + self.assertRaises(AttributeError, simport.load, + "TestSimport:missing") + + def test_good_load_internal(self): + self.assertEqual(dummy_function, + simport.load("TestSimport:dummy_function")) + self.assertEqual(DummyClass.method_a, + simport.load("TestSimport:DummyClass.method_a")) + + def test_good_load_local(self): + method = simport.load("tests|" + "localmodule:Foo.method_a") + import localmodule + + self.assertEqual(method, localmodule.Foo.method_a) + self.assertEqual(localmodule.function_a, + simport.load("localmodule:function_a")) + + def test_good_load_external(self): + method = simport.load("tests/external|" + "external.externalmodule:Blah.method_b") + + self.assertTrue('external.externalmodule' in sys.modules) + old = sys.modules['external.externalmodule'] + import external.externalmodule + + self.assertEqual(external.externalmodule, + sys.modules['external.externalmodule']) + self.assertEqual(old, external.externalmodule) + self.assertEqual(method, external.externalmodule.Blah.method_b) + + def test_import_class(self): + klass = simport.load("tests/external|" + "external.externalmodule:Blah") + import external.externalmodule + + self.assertEqual(klass, external.externalmodule.Blah) + + def test_local_class(self): + klass = simport.load("LocalClass", __name__) + self.assertEqual(klass, LocalClass)