diff --git a/openflexure_microscope/api/__init__.py b/openflexure_microscope/api/__init__.py index 9a39cfda..e69de29b 100644 --- a/openflexure_microscope/api/__init__.py +++ b/openflexure_microscope/api/__init__.py @@ -1,3 +0,0 @@ -__all__ = ["utilities", "views"] - -from . import utilities, views diff --git a/openflexure_microscope/api/v2/views/actions/__init__.py b/openflexure_microscope/api/v2/views/actions/__init__.py index 878c0c4e..15c287d7 100644 --- a/openflexure_microscope/api/v2/views/actions/__init__.py +++ b/openflexure_microscope/api/v2/views/actions/__init__.py @@ -6,7 +6,6 @@ from flask import Blueprint, url_for, jsonify from openflexure_microscope.api.utilities import blueprint_for_module from openflexure_microscope.utilities import get_docstring, description_from_view -from openflexure_microscope.api.views import MicroscopeView from . import camera, stage, system diff --git a/openflexure_microscope/api/views.py b/openflexure_microscope/api/views.py deleted file mode 100644 index 1ec58243..00000000 --- a/openflexure_microscope/api/views.py +++ /dev/null @@ -1,28 +0,0 @@ -from flask.views import MethodView - - -class MicroscopeView(MethodView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument. - """ - - def __init__(self, microscope, **kwargs): - - self.microscope = microscope - - MethodView.__init__(self, **kwargs) - - -class MicroscopeViewPlugin(MicroscopeView): - """ - Create a generic MethodView with a globally available - microscope object passed as an argument, and a plugin - reference stored in 'self'. Initially None. - """ - - def __init__(self, microscope, plugin=None, **kwargs): - - self.plugin = plugin - - MicroscopeView.__init__(self, microscope=microscope, **kwargs) diff --git a/openflexure_microscope/devel/__init__.py b/openflexure_microscope/devel/__init__.py index 95330291..05e9df1e 100644 --- a/openflexure_microscope/devel/__init__.py +++ b/openflexure_microscope/devel/__init__.py @@ -5,9 +5,6 @@ Here we include some classes used frequently in plugin development, as well as some Flask imports to simplify API route development """ -# Plugin classes -from openflexure_microscope.plugins import MicroscopePlugin -from openflexure_microscope.api.views import MicroscopeViewPlugin from openflexure_microscope.api.utilities import JsonResponse # Task management diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 694a8c32..8c7ca5f8 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -12,7 +12,6 @@ from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.camera.mock import MockStreamer from openflexure_microscope.utilities import serialise_array_b64 -from openflexure_microscope.plugins import PluginLoader from openflexure_microscope.common.lock import CompositeLock from openflexure_microscope.config import user_settings @@ -30,7 +29,6 @@ class Microscope: stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing background tasks using microscope hardware - plugins (:py:class:`openflexure_microscope.plugins.PluginLoader`): Mounting point for all microscope plugins """ def __init__(self): @@ -38,7 +36,6 @@ class Microscope: self.id = uuid.uuid4().hex self.name = self.id self.fov = [0, 0] - self.plugin_maps = [] self.camera = None self.stage = None @@ -48,10 +45,6 @@ class Microscope: # Apply settings loaded from file self.apply_settings(user_settings.load()) - # Create plugin mount-point and attach plugins from maps - self.plugins = PluginLoader(self) - self.attach_plugins(self.plugin_maps) - def __enter__(self): """Create microscope on context enter.""" return self @@ -116,25 +109,6 @@ class Microscope: logging.info("Reapplying settings to newly attached devices") self.apply_settings(settings_full) - def attach_plugins(self, plugin_maps: list): - """ - Automatically search for plugin maps in config, and attach. - """ - if plugin_maps: - for plugin_map in plugin_maps: - self.plugins.attach(plugin_map) - else: - logging.warning("No plugins specified. Skipping.") - - def reload_plugins(self): - """ - Empty the plugin mount and re-attach from config. - """ - logging.info("Tearing down existing PluginMount...") - self.plugins = PluginLoader(self) - logging.info("Repopulating PluginMount...") - self.attach_plugins(self.plugin_maps) - def has_real_stage(self): if hasattr(self, "stage") and not isinstance(self.stage, MockStage): return True @@ -183,8 +157,6 @@ class Microscope: self.name = config["name"] if "fov" in config: self.fov = config["fov"] - if "plugins" in config: - self.plugin_maps = config["plugins"] def read_settings(self, json_safe=False): """ @@ -197,12 +169,7 @@ class Microscope: don't get removed from the settings file. """ - settings_current = { - "id": self.id, - "name": self.name, - "fov": self.fov, - "plugins": self.plugin_maps, - } + settings_current = {"id": self.id, "name": self.name, "fov": self.fov} # If attached to a camera if self.camera: diff --git a/openflexure_microscope/plugins/__init__.py b/openflexure_microscope/plugins/__init__.py index c22207f5..e69de29b 100644 --- a/openflexure_microscope/plugins/__init__.py +++ b/openflexure_microscope/plugins/__init__.py @@ -1,17 +0,0 @@ -__all__ = [ - "module_from_file", - "load_plugin_class", - "load_plugin_module", - "class_from_map", - "PluginLoader", - "MicroscopePlugin", -] - -from .loader import ( - module_from_file, - load_plugin_class, - load_plugin_module, - class_from_map, - PluginLoader, - MicroscopePlugin, -) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py deleted file mode 100644 index 35f26c73..00000000 --- a/openflexure_microscope/plugins/loader.py +++ /dev/null @@ -1,349 +0,0 @@ -import importlib -import copy -import os -import collections -import inspect -import logging - -from openflexure_microscope.utilities import camel_to_snake, camel_to_spine -from openflexure_microscope.api.views import MicroscopeViewPlugin - - -class ConColors: - HEADER = "\033[95m" - OKBLUE = "\033[94m" - OKGREEN = "\033[92m" - WARNING = "\033[93m" - FAIL = "\033[91m" - ENDC = "\033[0m" - BOLD = "\033[1m" - UNDERLINE = "\033[4m" - - -def module_from_file(plugin_path): - # Expand environment variables in path string - plugin_path = os.path.expandvars(plugin_path) - # Expand user directory in path string - plugin_path = os.path.expanduser(plugin_path) - - # Check if the path is to a file - if not os.path.isfile(plugin_path): - logging.warning( - ConColors.FAIL - + "No valid plugin found at {}.".format(plugin_path) - + ConColors.ENDC - ) - return None, None, None - - else: - # Get name of plugin from the file - plugin_name = os.path.splitext(os.path.basename(plugin_path))[0] - - plugin_spec = importlib.util.spec_from_file_location(plugin_name, plugin_path) - plugin_module = importlib.util.module_from_spec(plugin_spec) - - return plugin_spec, plugin_module, plugin_name - - -def check_module(module_path): - """ - Used to check if a module exists, without ever importing it. - - This checks each nested level separately to avoid raising exceptions. - For example, checking "mymodule.submodule.subsubmodule" will first - check if 'mymodule' exists, then if it does, it will check - 'mymodule.submodule', and so on. - """ - module_split = module_path.split(".") - spec_path = "" - - for module_level in module_split: - spec_path += ".{}".format(module_level) - spec_path = spec_path.strip(".") - - logging.debug("Checking {}".format(spec_path)) - - # Try to find a module loader for the plugin path - plugin_spec = importlib.util.find_spec(spec_path) - - # If plugin spec doesn't exist, return False early - if plugin_spec is None: - return False - - # If all checks pass, return True - return True - - -def name_from_module(plugin_path): - path_array = plugin_path.split(".") - - # Truncate namespace of default plugins - if path_array[0:2] == ["openflexure_microscope", "plugins"]: - path_array = path_array[2:] - - return "/".join(path_array) - - -def load_plugin_module(plugin_path): - # If the loader was found (i.e. plugin probably exists) - if check_module(plugin_path): - try: - plugin_module = importlib.import_module(plugin_path) - plugin_name = name_from_module(plugin_path) - except Exception as e: - logging.warning("Error loading plugin.") - logging.error(e) - return None, None - - # If no loader was found, try finding a file from path - else: - plugin_spec, plugin_module, plugin_name = module_from_file(plugin_path) - # If a valid plugin was found - if plugin_spec and plugin_module: - # Execute the module, so we have access to it - plugin_spec.loader.exec_module(plugin_module) - - return plugin_module, plugin_name - - -def load_plugin_class(plugin_path, plugin_class_name): - plugin_module, plugin_name = load_plugin_module(plugin_path) - if plugin_module: - # Now try to extract the class - try: - plugin_class = getattr(plugin_module, plugin_class_name) - except AttributeError: - logging.warning( - ConColors.FAIL - + "Class {} does not exist in plugin {}. Skipping.".format( - plugin_class_name, plugin_path - ) - + ConColors.ENDC - ) - return None, None - else: - return plugin_class, plugin_name - else: - return None, None - - -def class_from_map(plugin_map): - plugin_arr = plugin_map.split(":") - - if not len(plugin_arr) == 2: - logging.warning( - ConColors.WARNING - + "Malformed plugin map {}. Skipping.".format(plugin_map) - + ConColors.ENDC - ) - return None, None - else: - return load_plugin_class(*plugin_arr) - - -class PluginLoader(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 - self._plugins = [] # List of plugin objects - self._legacy_plugins = {} # DEPRECATED: Dictionary of plugins with old names - self.forms = [] # List of plugin forms - logging.info("Creating plugin mount") - - @property - def state(self): - # DEPRECATED - logging.warning( - "PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version." - ) - return list(self._legacy_plugins.keys()) - - @property - def active(self): - return self._plugins - - def attach(self, plugin_map): - """ - Attach a MicroscopePlugin instance to the plugin mount. - - Args: - plugin_map (str): A plugin map describing the file or module to load a MicroscopePlugin child from. Maps should be in the format 'module.to.load:ClassName' or '/path/to/file:ClassName'. - """ - plugin_class, plugin_name = class_from_map(plugin_map) - - if plugin_class is not None: - - logging.debug(f"Loading plugin class {plugin_class}, {plugin_name}") - - plugin_name_python_safe = plugin_name.replace("/", "_") - - if plugin_class and plugin_name: - plugin_object = plugin_class() - - if hasattr( - self, plugin_name - ): # If a plugin with the same name is already attached. - logging.warning( - ConColors.WARNING - + "A plugin named {} has already been loaded. Skipping {}.".format( - plugin_name, plugin_map - ) - + ConColors.ENDC - ) - - elif isinstance( - plugin_object, BasePlugin - ): # If plugin_object is an instance of MicroscopePlugin - # Attach plugin_object to the plugin mount - setattr(self, plugin_name_python_safe, plugin_object) - # Store the plugin object, and it's properties - self._plugins.append(plugin_object) - # DEPRECATED: Store the plugin with it's old name - self._legacy_plugins[plugin_name] = plugin_object - - # Grant plugin access to the hardware - if isinstance(plugin_object, MicroscopePlugin): - plugin_object.microscope = self.parent - - logging.info( - ConColors.OKGREEN - + "Plugin {} loaded as {}.".format( - plugin_map, plugin_object._name - ) - + ConColors.ENDC - ) - - else: - logging.warning(f"Error loading plugin {plugin_map}. Moving on.") - - -class BasePlugin: - """ - Parent class for all plugins. - - Handles binding route views and forms. - """ - - def __init__(self): - self._views = ( - {} - ) # Key: Full, Python-safe ID. Val: Original rule, and view class - self._rules = {} # Key: Original rule. Val: View class - self._gui = None - - # If old api_views dictionary is found - if hasattr(self, "api_views"): - # Convert to new format - self._convert_old_api_views() - # If old api_form dictionary is found - if hasattr(self, "api_form"): - # Convert to new format - self._convert_old_api_form() - - @property - def views(self): - return self._views - - def add_view(self, rule, view_class, **kwargs): - # Remove all leading slashes from view route - cleaned_rule = rule - while cleaned_rule[0] == "/": - cleaned_rule = cleaned_rule[1:] - - # Expand the rule to include plugin name - full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule) - - view_id = cleaned_rule.replace("/", "_").replace("<", "").replace(">", "") - - # Create a Python-safe route ID - logging.debug(view_id) - - # Store route information in a dictionary - d = {"rule": full_rule, "view": view_class, "kwargs": kwargs} - - # Add view to private views dictionary - self._views[view_id] = d - # Store the rule expansion information - self._rules[rule] = self._views[view_id] - - @property - def gui(self): - print(self._gui) - # Handle missing/no GUI - if not self._gui: - return None - # Handle GUI as a dictionary-returning callable function - elif isinstance(self._gui, collections.Callable): - api_gui = self._gui() - # Handle GUI as a static dictionary - elif isinstance(self._gui, dict): - api_gui = copy.deepcopy(self._gui) - # Handle borked GUI - else: - raise ValueError( - "GUI must be a dictionary, or a dictionary-returning function with no arguments." - ) - - api_gui["id"] = self._name - - if "forms" in api_gui and isinstance(api_gui["forms"], list): - for form in api_gui["forms"]: - if "route" in form and form["route"] in self._rules.keys(): - form["route"] = self._rules[form["route"]]["rule"] - else: - logging.warn( - "No valid expandable route found for {}".format(form["route"]) - ) - return api_gui - - def set_gui(self, form_dictionary: dict): - self._gui = form_dictionary - - def _convert_old_api_views(self): - for view_route, view_class in self.api_views.items(): - self.add_view(view_route, view_class) - - def _convert_old_api_form(self): - self.set_gui(self.api_form) - - @property - def _name(self): - return self.__class__.__name__ - - @property - def _name_python_safe(self): - return camel_to_snake(self._name) - - @property - def _name_uri_safe(self): - return camel_to_spine(self._name) - - def _full_name(self): - module = self.__class__.__module__ - if module is None or module == str.__class__.__module__: - return self.__class__.__name__ # Avoid reporting __builtin__ - else: - return module + "." + self.__class__.__name__ - - -class MicroscopePlugin(BasePlugin): - """ - Parent class for all microscope plugins. - - Initially only defines an empty object for microscope. All plugins - must be an instance of this class to successfully attach to PluginMount. - """ - - api_views = {} # Initially empty dictionary of API views associated with the plugin - - def __init__(self): - BasePlugin.__init__(self) - self.microscope = ( - None - ) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object