Skip to content

Commit

Permalink
Refs #4226. Add plugin loader for Python framework.
Browse files Browse the repository at this point in the history
Adds a generic plugin (module) loader for importing additional
python modules at start up
  • Loading branch information
martyngigg committed Dec 1, 2011
1 parent 4e7c47e commit 1907ead
Show file tree
Hide file tree
Showing 22 changed files with 216 additions and 10 deletions.
1 change: 1 addition & 0 deletions Code/Mantid/Framework/PythonInterface/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ set ( TEST_PY_FILES
test/python/ImportModuleTest.py
test/python/LoggerTest.py
test/python/MatrixWorkspaceTest.py
test/python/PythonPluginsTest.py
test/python/PropertyWithValueTest.py
test/python/PythonAlgorithmTest.py
test/python/SimpleAPITest.py
Expand Down
10 changes: 10 additions & 0 deletions Code/Mantid/Framework/PythonInterface/mantid/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@
algorithm_factory = get_algorithm_factory()
analysis_data_svc = get_analysis_data_service()

###############################################################################
# Starting the FrameworkManager loads the C++ plugin libraries
# we need to load in the Python plugins as well
###############################################################################
import mantid.kernel.plugins as _plugins
# Algorithms
from mantid.kernel import config as _cfg
# Disabled for the time being as all algorithms are of the old kind
#_plugins.load(_cfg['pythonalgorithm.directories'])

###############################################################################
# When in GUI mode we want to be picky about algorithm execution as we
# currently can't run the scripts in a separate thread:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ set ( PY_FILES
__init__.py
dlopen.py
funcreturns.py
plugins.py
)

#############################################################################################
Expand Down
81 changes: 81 additions & 0 deletions Code/Mantid/Framework/PythonInterface/mantid/kernel/plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""
Defines functions to dynamically load Python modules.
These modules may define extensions to C++ types, e.g.
algorithms, fit functions etc.
"""
import os as _os
import imp as _imp
from mantid.kernel import Logger

class PluginLoader(object):

def __init__(self, filepath):
if not _os.path.isfile(filepath):
raise ValueError("PluginLoader expects a single filename. '%s' does not point to an existing file" % filepath)
if not filepath.endswith('.py'):
raise ValueError("PluginLoader expects a filename ending with .py. '%s' does not have a .py extension" % filepath)
self._filepath = filepath
self._logger = Logger.get("PluginLoader")

def run(self):
"""
Load the module we are pointing at and return
the module object.
Any ImportErrors raised are not caught and are passed
on to the caller
"""
pathname = self._filepath
name = _os.path.basename(pathname) # Including extension
name = _os.path.splitext(name)[0]
self._logger.debug("Loading python plugin %s" % pathname)
return _imp.load_source(name, pathname)

def load(path):
"""
High-level function to import the module(s) on the given path.
The module is imported using __import__ so any code not defined
inside an if __name__ == '__main__' block is executed.
@param path :: If the path is a filename load the file; if the
path points to a directory load all files in the directory
recursively; if the path contains a list of directories then
all files in each are loaded in turn
@return A list of the names of the loaded modules. Note this
will not included modules that will have attempted to be
reloaded but had not been changed
"""
loaded = []
if _os.path.isfile(path) and path.endswith('.py'): # Single file
loader = PluginLoader(path)
module = loader.run()
loaded.append(module.__name__)
elif _os.path.isdir(path): # Directory
loaded.extend(load_from_dir(path))
else: # a list
if ';' in path:
path = split(';')
if type(path) is list: # Call load again for each one
for p in path:
loaded.extend(load(p))

return loaded

def load_from_dir(directory):
"""
Load all modules in the given directory
@param directory :: A path that must point to a directory
"""
if not _os.path.isdir(directory):
raise RuntimeError("The path given does not point to an existing directory")
loaded = []
for root, dirs, files in _os.walk(directory):
for f in files:
filename = _os.path.join(root, f)
loaded.extend(load(filename))

return loaded

Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ def test_get_registered_algs_returns_dictionary_of_known_algorithms(self):
self.assertTrue( 'ConvertUnits' in all_algs )
# 3 versions of LoadRaw
self.assertEquals( len(all_algs['LoadRaw']), 3 )
self.assertEquals( all_algs['LoadRaw'], [1,2,3] )
self.assertEquals( all_algs['LoadRaw'], [1,2,3] )

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,6 @@ def test_algorithm_registration_with_invalid_object_throws(self):
except ValueError:
error = True
self.assertTrue(error)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ def test_execute_succeeds_with_valid_props(self):
self.assertEquals(alg.get_property('NSpec').name, 'NSpec')
ws = alg.get_property('OutputWorkspace').value
self.assertTrue(ws.get_memory_size() > 0.0 )

if __name__ == '__main__':
unittest.main()

Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,6 @@ def test_removing_item_invalidates_extracted_handles(self):
self.assertTrue(succeeded, "DataItem handle should be valid and allow function calls")
analysis_data_svc.remove(wsname)
self.assertRaises(RuntimeError, ws_handle.id)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,5 @@ def _clean_up_test_areas(self):
except OSError:
pass

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ def test_alg_get_property_converts_to_this(self):
alg = run_algorithm('Load', Filename='LOQ48127.raw', OutputWorkspace='tmp', SpectrumMax=1)
prop = alg.get_property("Filename")
self.assertEquals(type(prop), FileProperty)
self.assertTrue('value' in dir(prop)) # Do we have a value method
self.assertTrue('value' in dir(prop)) # Do we have a value method


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,7 @@ def PyExec(self):
self._test_obj.assertFalse(alg.__async__)

top_level = TestAlg(self)
top_level.PyExec()
top_level.PyExec()

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,7 @@ def test_that_clearing_mru_does_not_raise_an_error(self):
error_raised = False
except:
error_raised = True
self.assertFalse(error_raised)
self.assertFalse(error_raised)

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ def test_import_succeeds(self):

def test_on_import_gui_flag_is_set_to_false_here(self):
import mantid
self.assertEquals(False, mantid.__gui__)
self.assertEquals(False, mantid.__gui__)

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ def test_logger_creation_does_not_raise_an_error(self):
for att in attrs:
if not hasattr(logger, att):
self.fail("Logger object does not have the required attribute '%s'" % att)



if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ def _do_numpy_comparison(self, workspace, x_np, y_np, e_np):
self.assertEquals(e_np[i][j], workspace.read_e(i)[j])
# Extra X boundary
self.assertEquals(x_np[i][blocksize], workspace.read_x(i)[blocksize])

if __name__ == '__main__':
unittest.main()

Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,5 @@ def test_set_property_of_vector_int_succeeds_with_numpy_array_of_int_type(self):
def test_set_property_of_vector_int_succeeds_with_numpy_array_of_int_type(self):
self._do_vector_int_numpy_test('WorkspaceIndexList')

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ def test_alg_with_overridden_attrs(self):
self.assertEquals(alg.version(), 2)
self.assertEquals(alg.category(), "BestAlgorithms")


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import unittest
import os
import shutil
import sys

import mantid.kernel.plugins as plugins
from mantid import algorithm_factory, algorithm_mgr

__TESTALG__ = \
"""from mantid import Algorithm, register_algorithm
class TestPyAlg(Algorithm):
def PyInit(self):
pass
def PyExec(self):
pass
register_algorithm(TestPyAlg)
"""

class PythonPluginsTest(unittest.TestCase):

def setUp(self):
# Make a test directory and test plugin
self._testdir = os.path.join(os.getcwd(), 'PythonPluginsTest_TmpDir')
try:
os.mkdir(self._testdir)
except OSError:
pass # Already exists, maybe it was not removed when a test failed?
filename = os.path.join(self._testdir, 'TestPyAlg.py')
if not os.path.exists(filename):
plugin = file(filename, 'w')
plugin.write(__TESTALG__)
plugin.close()

def tearDown(self):
try:
shutil.rmtree(self._testdir)
except shutil.Error:
pass

def test_loading_python_algorithm_increases_registered_algs_by_one(self):
loaded = plugins.load(self._testdir)
self.assertTrue(len(loaded) > 0)
expected_name = 'TestPyAlg'
# Has the name appear in the module dictionary
self.assertTrue(expected_name in sys.modules)
# Do we have the registered algorithm
algs = algorithm_factory.get_registered_algorithms(True)
self.assertTrue(expected_name in algs)
# Can it be created?
try:
test_alg = algorithm_mgr.create_unmanaged(expected_name)
self.assertEquals(expected_name, test_alg.name())
self.assertEquals(1, test_alg.version())
except RuntimeError, exc:
self.fail("Failed to create plugin algorithm from the manager: '%s' " %s)


if __name__ == '__main__':
unittest.main()
3 changes: 3 additions & 0 deletions Code/Mantid/Framework/PythonInterface/test/python/QuatTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ def test_rotate(self):
p = Quat(45., V3D(0,0,1))
p.rotate(v);
self.assertEquals(v, V3D(a, a, 0))

if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -122,4 +122,8 @@ def test_that_dialog_call_raises_runtime_error(self):
except RuntimeError, exc:
msg = str(exc)
if msg != "Can only display properties dialog in gui mode":
self.fail("Dialog function raised the correct exception type but the message was wrong")
self.fail("Dialog function raised the correct exception type but the message was wrong")


if __name__ == '__main__':
unittest.main()
4 changes: 4 additions & 0 deletions Code/Mantid/Framework/PythonInterface/test/python/V3DTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ def test_norm(self):
def test_norm2(self):
p = V3D(1.0,-5.0,8.0);
self.assertAlmostEquals(p.norm2(), 90.0)


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ def test_that_one_cannot_be_instantiated(self):
except RuntimeError: # For some reason self.assertRaises doesn't catch this
error = True
self.assertTrue(error, True)

if __name__ == '__main__':
unittest.main()

0 comments on commit 1907ead

Please sign in to comment.