From 3c096e9b53c109f564b306821438824ca65edfc2 Mon Sep 17 00:00:00 2001 From: jtc42 Date: Tue, 19 Nov 2019 19:19:54 +0000 Subject: [PATCH] Overhauled v2 plugin attaching --- .../api/v1/blueprints/plugins.py | 5 +- .../api/v2/blueprints/plugins.py | 108 ++++------- .../plugins/default/scan/plugin.py | 2 + openflexure_microscope/plugins/loader.py | 176 ++++++++++++------ .../plugins/testing/form_example/plugin.py | 2 + openflexure_microscope/utilities.py | 9 + 6 files changed, 167 insertions(+), 135 deletions(-) diff --git a/openflexure_microscope/api/v1/blueprints/plugins.py b/openflexure_microscope/api/v1/blueprints/plugins.py index c1d8fc97..f172b79b 100644 --- a/openflexure_microscope/api/v1/blueprints/plugins.py +++ b/openflexure_microscope/api/v1/blueprints/plugins.py @@ -39,10 +39,7 @@ def construct_blueprint(microscope_obj): all_routes = [] # For each plugin attached to the microscope object - for plugin_representation in microscope_obj.plugins.active: - - plugin_obj = plugin_representation["plugin"] - plugin_name = plugin_representation["name"] + for plugin_name, plugin_obj in microscope_obj.plugins._legacy_plugins.items(): # If plugin contains valid endpoints if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): diff --git a/openflexure_microscope/api/v2/blueprints/plugins.py b/openflexure_microscope/api/v2/blueprints/plugins.py index 50f298f0..b4709ce7 100644 --- a/openflexure_microscope/api/v2/blueprints/plugins.py +++ b/openflexure_microscope/api/v2/blueprints/plugins.py @@ -20,20 +20,34 @@ def plugins_representation(plugin_loader_object: PluginLoader): dict: Dictionary representation of all plugins """ plugins = [] + + print(plugin_loader_object.active) + for plugin in plugin_loader_object.active: + logging.info(f"Representing plugin {plugin._name}") d = { - "name": plugin["name"], - "plugin": str(plugin["plugin"]), - "routes": plugin["routes"], - "form": plugin["form"], + "name": plugin._name, + "plugin": str(plugin), + "views": {}, + "form": plugin.form, } - for route in d["routes"]: - route_id = route["id"] - uri = url_for(f"v2_plugins_blueprint.{route_id}") - route["links"]["self"] = uri + for view_id, view in plugin.views.items(): + logging.info(f"Representing view {view_id}") + uri = url_for(f"v2_plugins_blueprint.{view_id}") + # Make links dictionary if it doesn't yet exist + view_d = { + "links": {"self": uri} + } + + d["views"][view_id] = view_d + + print("\n") + print(d) + print("\n") plugins.append(d) + print(plugins) return plugins @@ -66,69 +80,20 @@ def construct_blueprint(microscope_obj): all_routes = [] # For each plugin attached to the microscope object - for plugin_representation in microscope_obj.plugins.active: + for plugin in microscope_obj.plugins.active: - plugin_obj = plugin_representation["plugin"] - plugin_name = plugin_representation["name"] - - # If plugin contains valid endpoints - if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict): - - # We'll keep a record of how each route was expanded - expanded_routes = {} - - # For each defined endpoint - for view_route, view_class in plugin_obj.api_views.items(): - - # Remove all leading slashes from view route - cleaned_route = view_route - while cleaned_route[0] == "/": - cleaned_route = cleaned_route[1:] - - # Construct a full view route from the plugin name - full_view_route = "/{}/{}".format(plugin_name, cleaned_route) - logging.debug(full_view_route) - - # Record how the view_route got expanded - expanded_routes[view_route] = full_view_route - - # Check if endpoint name clashes - if full_view_route not in all_routes and issubclass( - view_class, MicroscopeViewPlugin - ): - # Add route to main route dictionary - all_routes.append(full_view_route) - - # Create a Python-safe name for the route - plugin_route_id = "plugin{}".format(full_view_route).replace( - "/", "_" - ) - - # Add route to the plugins blueprint - blueprint.add_url_rule( - full_view_route, - view_func=view_class.as_view( - plugin_route_id, - microscope=microscope_obj, - plugin=plugin_obj, - ), - ) - - # Add route to the plugin representation dictionary - route_representation = { - "id": plugin_route_id, - "route": full_view_route, - "links": {}, - } - plugin_representation["routes"].append(route_representation) - - else: - warnings.warn( - "An endpoint /{} has already been loaded. Skipping {}.".format( - full_view_route, view_class - ) - ) + for plugin_view_id, plugin_view in plugin.views.items(): + # Add route to the plugins blueprint + blueprint.add_url_rule( + plugin_view["rule"], + view_func=plugin_view["view"].as_view( + plugin_view_id, + microscope=microscope_obj, + plugin=plugin, + ), + ) + """ # If plugin includes an API form if hasattr(plugin_obj, "api_form") and isinstance( plugin_obj.api_form, dict @@ -152,9 +117,6 @@ def construct_blueprint(microscope_obj): # Store the complete form in Microscope().plugin.form plugin_representation["form"] = api_form_info print(microscope_obj.plugins.forms) + """ - else: - warnings.warn( - "No valid 'api_views' dictionary found in {}".format(plugin_obj) - ) return blueprint diff --git a/openflexure_microscope/plugins/default/scan/plugin.py b/openflexure_microscope/plugins/default/scan/plugin.py index b63abe0e..9d1d02d3 100644 --- a/openflexure_microscope/plugins/default/scan/plugin.py +++ b/openflexure_microscope/plugins/default/scan/plugin.py @@ -58,6 +58,8 @@ class ScanPlugin(MicroscopePlugin): api_views = {"/tile": TileScanAPI} def __init__(self): + MicroscopePlugin.__init__(self) + self.images_to_be_captured: int = 1 update_task_data({"images_to_be_captured": self.images_to_be_captured}) diff --git a/openflexure_microscope/plugins/loader.py b/openflexure_microscope/plugins/loader.py index 4aaef28e..2a2eac05 100644 --- a/openflexure_microscope/plugins/loader.py +++ b/openflexure_microscope/plugins/loader.py @@ -4,6 +4,7 @@ import os import inspect import logging +from openflexure_microscope.utilities import camel_to_snake, camel_to_spine from openflexure_microscope.api.views import MicroscopeViewPlugin @@ -83,7 +84,6 @@ def name_from_module(plugin_path): def load_plugin_module(plugin_path): - # If the loader was found (i.e. plugin probably exists) if check_module(plugin_path): try: @@ -140,50 +140,6 @@ def class_from_map(plugin_map): return load_plugin_class(*plugin_arr) -def expand_views(plugin_object, plugin_name): - routes = [] - - # If plugin contains valid endpoints - if hasattr(plugin_object, "api_views") and isinstance(plugin_object.api_views, dict): - - # We'll keep a record of how each route was expanded - expanded_routes = {} - - # For each defined endpoint - for view_route, view_class in plugin_object.api_views.items(): - - # Remove all leading slashes from view route - cleaned_route = view_route - while cleaned_route[0] == "/": - cleaned_route = cleaned_route[1:] - - # Construct a full view route from the plugin name - full_view_route = "/{}/{}".format(plugin_name, cleaned_route) - logging.debug(full_view_route) - - # Record how the view_route got expanded - expanded_routes[view_route] = full_view_route - - # Check if endpoint name clashes - if issubclass(view_class, MicroscopeViewPlugin): - - # Create a Python-safe name for the route - plugin_route_id = "plugin{}".format(full_view_route).replace( - "/", "_" - ) - - # Add route to the plugin representation dictionary - route_representation = { - "id": plugin_route_id, - "route": full_view_route, - "links": {}, - } - - routes.append(route_representation) - - return routes - - class PluginLoader(object): """ A mount-point for all loaded plugins. Attaches to a Microscope object. @@ -195,6 +151,7 @@ class PluginLoader(object): 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") @@ -221,13 +178,15 @@ class PluginLoader(object): 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 + self, plugin_name ): # If a plugin with the same name is already attached. logging.warning( ConColors.WARNING @@ -238,24 +197,19 @@ class PluginLoader(object): ) elif isinstance( - plugin_object, MicroscopePlugin + 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( - { - "name": plugin_name, - "python_name": plugin_name_python_safe, - "plugin": plugin_object, - "routes": [], - "form": None, - } - ) + self._plugins.append(plugin_object) + # DEPRECATED: Store the plugin with it's old name + self._legacy_plugins[plugin_name_python_safe] = plugin_object # Grant plugin access to the hardware - plugin_object.microscope = self.parent + if isinstance(plugin_object, MicroscopePlugin): + plugin_object.microscope = self.parent logging.info( ConColors.OKGREEN @@ -267,7 +221,112 @@ class PluginLoader(object): logging.warning(f"Error loading plugin {plugin_map}. Moving on.") -class MicroscopePlugin: +class BasePlugin: + """ + Parent class for all plugins. + + Handles binding route views and forms. + """ + + def __init__(self): + self._views = {} + self._rules = {} + self._form = 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() + + print("\n\n NAME: " + self._name + "\n\n") + print(self.views) + print(self.form) + + @property + def views(self): + return self._views + + def add_view(self, rule, view_class): + # 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) + + # Create a Python-safe route ID + view_id = cleaned_rule.replace("/", "_") + full_view_id = f"{self._name_python_safe}_{view_id}" + + # Store route information in a dictionary + d = { + "rule": full_rule, + "view": view_class + } + + # Add view to private views dictionary + self._views[full_view_id] = d + # Store the rule expansion information + self._rules[rule] = self._views[full_view_id] + + @property + def form(self): + if not self._form: + return None + + api_form_info = copy.deepcopy(self._form) + api_form_info["id"] = self._name + + if "forms" in api_form_info and isinstance( + api_form_info["forms"], list + ): + for form in api_form_info["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_form_info + + def set_form(self, form_dictionary: dict): + self._form = 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_form(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. @@ -278,6 +337,7 @@ class MicroscopePlugin: 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 diff --git a/openflexure_microscope/plugins/testing/form_example/plugin.py b/openflexure_microscope/plugins/testing/form_example/plugin.py index 42acb932..aabf6880 100644 --- a/openflexure_microscope/plugins/testing/form_example/plugin.py +++ b/openflexure_microscope/plugins/testing/form_example/plugin.py @@ -23,6 +23,8 @@ class ExamplePlugin(MicroscopePlugin): api_views = {"/do": DoAPI, "/task": TaskAPI} def __init__(self): + MicroscopePlugin.__init__(self) + self.val_int = 10 self.val_str = "Hello" self.val_radio = "First" diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 69396fc5..943e54c1 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -1,3 +1,4 @@ +import re import copy import operator from collections import abc @@ -5,6 +6,14 @@ from functools import reduce from contextlib import contextmanager +def camel_to_snake(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + +def camel_to_spine(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1-\2', s1).lower() + @contextmanager def set_properties(obj, **kwargs): """A context manager to set, then reset, certain properties of an object.