diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 75449b07..a5e51930 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -8,6 +8,8 @@ import numpy as np from openflexure_stage import OpenFlexureStage from .camera.pi import StreamingCamera +from .plugins import PluginMount + class Microscope(object): """ @@ -22,6 +24,9 @@ class Microscope(object): def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage): self.attach(camera, stage) + # Create plugin mountpoint + self.plugin = PluginMount(self) + def __enter__(self): """Create microscope on context enter.""" return self diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py new file mode 100644 index 00000000..883f1f82 --- /dev/null +++ b/openflexure_microscope/plugins/loader.py @@ -0,0 +1,116 @@ +import importlib +import os +import warnings +import logging + +MAIN_MODULE = '__init__' +HERE = os.path.abspath(os.path.dirname(__file__)) +DEFAULT_PLUGIN_PATH = os.path.join(HERE, 'default') + + +def search_plugin_dirs(plugin_dirs, include_default=True): + """ + Search through, and load from, a list of plugin directories. + + Args: + plugin_dirs (list): List of strings of plugin directories. + include_default (bool): Also load plugins from the module default directory (DEFAULT_PLUGIN_PATH) + """ + global DEFAULT_PLUGIN_PATH + + if include_default: # If including default plugins + plugin_dirs.append(DEFAULT_PLUGIN_PATH) # Add default directory to the search paths + + logging.debug(plugin_dirs) + + plugins = [] # List of loaded plugins + for plugin_dir in plugin_dirs: # For each plugin directory + logging.debug("Searching {}".format(plugin_dir)) + plugins.extend(find_plugins(plugin_dir)) # Find plugin folders, and load into list + + return plugins + + +def find_plugins(plugin_dir): + """ + Find all plugins residing within a directory + + Args: + plugin_dir (str): String of directory to be searched + """ + plugins = [] + plugins_folders = os.listdir(plugin_dir) + + loader_details = ( + importlib.machinery.SourceFileLoader, + importlib.machinery.SOURCE_SUFFIXES + ) + + for i in plugins_folders: + plugin_folder = os.path.join(plugin_dir, i) + logging.info(plugin_folder) + + if not os.path.isdir(plugin_folder): + continue + if not MAIN_MODULE + '.py' in os.listdir(plugin_folder): + continue + + module_spec = importlib.machinery.FileFinder(plugin_folder, loader_details).find_spec(MAIN_MODULE) + + plugins.append(module_spec) + return plugins + + +def load_plugin(module_spec): + """ + Load a source file from a given spec. + + Args: + module_spec: Module spec of module to be returned + """ + module = importlib.util.module_from_spec(module_spec) + module_spec.loader.exec_module(module) + return module + + +class PluginMount(object): + """ + A mount-point for all loaded plugins. Attaches to a Microscope object. + + Args: + parent (:py:class:`openflexure_microscope.microscope.Microscope`): The parent Microscope object to attach to. + """ + def __init__(self, parent): + self.parent = parent + print("Creating plugin mount") + + def attach(self, plugin_module): + if not hasattr(plugin_module, 'PLUGINS') or not isinstance(plugin_module.PLUGINS, dict): + raise Exception("No falid PLUGINS dictionary found in {}".format(plugin_module)) + + for plugin_name, plugin_class in plugin_module.PLUGINS.items(): + + plugin_object = plugin_class() + if hasattr(self, plugin_name): + warnings.warn("A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_class)) + else: + setattr(self, plugin_name, plugin_object) + + # Grant plugin access to the hardware + assert(isinstance(plugin_object, MicroscopePlugin)) + plugin_object.camera = self.parent.camera + plugin_object.stage = self.parent.stage + + print("Adding plugin: {}".format(plugin_name)) + + +class MicroscopePlugin(): + """ + Parent class for all microscope plugins. + + Initially only defines an empty object for camera and stage. All plugins + must be an instance of this class to successfully attach to PluginMount. + """ + def __init__(self): + self.camera = None #: :py:class:`openflexure_microscope.camera.pi.StreamingCamera`: Picamera object + self.stage = None #: :py:class:`openflexure_stage.stage.OpenFlexureStage`: OpenFlexure stage object